【レポート】Concurrency in Rust – HACKER TACKLE 2018

2018.02.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

本記事は2018年開催HACKER TACKLE - ハカタクル?のセッション「Concurrency in Rust」のレポートです。 スライドが公開されたらリンクを追加します。

すべてのセッションについてはこちらの記事をご覧ください

(2018/2/20 14時追記) 登壇者のAlexさんに連絡したところ、スライドのファイルをいただけました。 ブログに載っけて良いよ!と快諾いただいております。

concurrency-in-rust-fukuoka

概要

HACKER TACKLE公式サイト からの引用です。

The Rust programming language purports the bold claim that it guarantees thread safety while retaining the ability to write zero-cost abstractions. In this talk we'll explore precisely how Rust can make such a claim. We'll also explore the ecosystem that makes up the concurrency toolkit in Rust to see how these language principles are extended to common abstractions such as channels, thread pools, work stealing algorithms, concurrent data structures, and asynchronous I/O.

並行プログラミングでよく起きる問題の解決方法を中心に、Rust言語の特徴を紹介されていました。

スピーカー

  • Alex Crichton - Mozilla Corporation

Rustのコア開発メンバーのAlexさんです。GitHubでRust関連のリポジトリを見ているとよく見かける方ですね。

セッションレポート

Concurrency?

  • 最近のマイクロプロセッサの動向
    • 周波数よりも論理コア数が増える傾向にある
  • 並行プログラミングで高速化したい
  • けど、並行プログラミングは難しい
  • 並行プログラミングのハマりどころ
    • Data Races: データ競合
    • Race Conditions: 競合状態
    • Deadlocks: デッドロック
    • Use after free: 破棄した領域へのアクセス
    • Double free: 二重開放
    • セキュリティホールになりがち
  • Rust使えばこのあたりの問題に怯えることなく、安全に並行プログラミングできる

Rust?

概要

  • 抽象化のコストなし
  • メモリ安全
  • データ競合なし
  • これらを達成することで、生産性の高いシステムプログラミングを可能にする

What's safety?

  • C++実装の例示、std::vectorでダングリングポインタになる例を紹介
  • データの更新(mutation)契機でエイリアスポインタがダングリングポインタになる

Rust's Solution

  • どうやって解決するの?
    • OwnershipとBorrowingを活用
    • ランタイムチェック不要
    • GC不要
    • データ競合なし
  • Ownershipって?
    • 所有権についての説明
    • Rustだと所有権が移った後にアクセスするとコンパイルエラー
  • Borrowingって?
    • 借用についての説明
  • Safety in Rust
    • aliasing + mutationを静的に防ぐ
    • Ownershipでdouble-freeを防ぐ
    • Borrowingでuse-after-freeを防ぐ
    • これらの仕組みを活用して、Segmentation Faultを防ぐ

Libraries

  • 並行プログラミングの対応について
    • 以前は言語のビルトイン機能でメッセージパッシングできた
    • 今はマルチパラダイム対応のためライブラリで対応している
    • ライブラリはOwnershipとBorrowingを活用してデータ競合を避ける
  • std::thread
    • クロージャ内でアクセスする変数の所有権について
    • デフォルトだと所有権は移動しない、クロージャ内からクロージャ外の変数にアクセスするとコンパイルエラー
    • move で所有権を移動させるとクロージャ内でアクセスできるようになる。ただしクロージャの外で同変数にアクセスするとコンパイルエラー
  • std::sync::Arc
    • スレッド間でメモリを安全に共有するための仕組み
    • ただしmutation不可
  • std::sync::Mutex
    • スレッド間でメモリを安全に共有するための仕組み
    • ロックを取得した後で参照・更新できるようになる
  • std::sync::mpsc
    • スレッド間コミュニケーションのためのチャネル機構
    • 複数の送信者が1つの受信者にメッセージを送信する
  • rayon
    • コレクションを並列処理するためのライブラリ
    • .iter().par_iter() に置き換えるだけで利用できる
    • ただしクロージャ内で外側の変数にアクセスするとコンパイルエラー
  • 100% Safe
    • 静的なチェックでと仕組みで危険なコードを書けないようにしている
    • Segmentation Faultが発生しない
    • データ競合が発生しない
    • 二重開放に悩まされない

ここまでが発表の内容です。

スライドの内容を試してみた

検証環境

  • macOS Sierra 10.12.6
  • Apple LLVM version 9.0.0 (clang-900.0.39.2)
  • rustc 1.24.0 (4d90ac38c 2018-02-12)

What's safety?のダングリングポインタ

以下はスライドに記載されていたものではありませんが、概ね下記コードのような内容で例示されていました。

C++の場合

main.cc

#include <cstdio>
#include <cstdint>
#include <vector>

using namespace std;

int main(int argc, char** argv)
{
  vector<uint8_t> v(5);   // 初期サイズ5
  const auto elm = &v[0]; // 先頭要素のアドレス
  
  // 要素を追加する、この時点でelmがダングリングポインタになる
  v.push_back(1);
  
  // 先頭要素のアドレスが変更されていることを確認する
  printf(" elm = %p\n", elm);
  printf("&v[0]= %p\n", &v[0]);
  
  // 解放済みの領域へのアクセス
  printf(" elm = %u\n", *elm);

  return 0;
}

ビルドして実行します。

# ビルド
$ clang++ -std=c++11 -o main main.cc

# 実行
$ ./main
 elm = 0x100b58f90
&v[0]= 0x100b58fe0

アドレスの数値は実行環境によって違いがあると思いますが、事前に elm 変数に取得したアドレスと &v[0] のアドレスが一致しないことが確認できます。 確認のためValgrindで実行したところ、解放済みの領域へのアクセスを検知しました。

==73323== Invalid read of size 1
==73323==    at 0x1000008DE: main (in ./test)
==73323==  Address 0x100b58f90 is 0 bytes inside a block of size 5 free'd

Rustの場合

main.rs

fn main() {
    // 初期サイズ5
    let mut v = vec![0, 5];
    let elm = &v[0];

    // 要素を追加
    v.push(1);

    // アドレスを確認
    println!(" elm  = {:p}", elm);
    println!("&v[0] = {:p}", &v[0]);
}

ビルドします。

$ cargo build
...
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:5
  |
3 |     let elm = &v[0];
  |                - immutable borrow occurs here
4 |
5 |     v.push(1);
  |     ^ mutable borrow occurs here
...
9 | }
  | - immutable borrow ends here

error: aborting due to previous error

セッションの内容どおり、Rustだとコンパイルエラーになりました。 念のため v.push(1); をコメントアウトして確認したところ、今度は問題なくコンパイルできるようになりました。

終わりに

並行プログラミングは、ロックの扱いやタイミング依存で発生する不具合など頭を抱えることが多かったですが、Rust言語はこういった問題をそもそも発生させなくなることを目指して開発されている印象を受けました。

所有権やライフタイムなど、経験則で気をつけていた作法が言語の概念として説明されていて、色々と納得することが多かったです。

そしてRust言語開発チームのコアメンバーの話を直接聞くことができて感動しっぱなしでした。 モチベーションがあがってるところで近いうちにツール作成などで試していきたいと思います。

参考