kanejaku.org

RustのSend

Publish date 25 Dec 2018

疑問: RustのライブラリAPIの定義を見ていたら制約にT: Sendと書いてあった。なぜこのAPIはSendを要求するのか。

短い答え: そのAPIはスレッドをまたいで値を渡す。その値の所有権をスレッド越しに渡してもデータ競合を起こさない、ということを要求するため。Sendでない型はたいていRc<T>かポインタ。

これはAPIドキュメントを見て疑問の思ったことを調べて、使う側からどう解釈すべきか、という視点から理解しようとするシリーズの二番目の記事となる。前回に引き続き、お題はthread::spawn。自分の浅い理解から書いているので間違いを含んでいるかもしれない。

thread::spawn()に渡すクロージャの制約はFnOnce() + Send +'staticなので前回の記事で言及したFnOnce()に加えてSendを実装していないといけない。APIドキュメントには値を安全に別スレッドに送るためにSendが必要だ、と書いてある。でもRustには所有権の概念があるから値を渡せばそれで安全になるんじゃないのか。逆に言うとスレッド越しに渡すと安全じゃない値ってなんなんだろうか。調べた感じだと、大体二つに分類される。

ひとつはRc<T>Rc<T>な値は参照カウンタとともにヒープ上に確保される。レイアウトはこんな感じ:

+--------------+------------+-------+
|strong ref cnt|weak ref cnt| value |
+--------------+------------+-------+

Rc::clone()を呼ぶと値を複製できる。つまり所有権を持つ変数を複数作れる。Rc::clone()はヒープ上の参照カウントを増やすのだけど、この参照カウンタはアトミック変数じゃないし、排他制御も行わない。これらの値を異なるスレッドで生成したりドロップしたりしたら参照カウンタのデータ競合が起こる。なのでRc<T>Sendではない。

一方、Rc<T>と同じように内部可変性(interior mutability)を使っているCell<T>RefCell<T>は、TSendであればCell/RefCellもSendになる。Cell/RefCellがスレッド安全でなくなるのは、Cell/RefCellへの参照をスレッド間で渡したときであって、所有権を渡すこと自体は安全な操作となる。

もうひとつのSendでない型はポインタ。*const u8とか*mut u8とか。これらがSendではない理由はRc<T>ほど自明ではない。ポインタをスレッド間で渡すこと自体はデータ競合を起こさない。安全でなくなるのはポインタをdereferenceするときだけど、そのときはどのみちunsafeでくくらなければいけない。だったらポインタをSendである、としても安全性は損なわないような…などと思いつつ調べていたらNomiconに言及があった。主に書き手に注意喚起する目的らしい。言語設計上の選択だったんだろう。

ちなみに参照&TのほうはSendを実装している。理由はコンパイラが借用ルールを使って安全でない参照の利用を検知できるから。

自分で定義した型はどうか。型がSendであるかどうかは基本的にコンパイラがよしなに判断してくれる。内包する型が全部Sendを実装していれば、その型もSendになる。内包する型のうち一つでもSendでない型があれば、定義した型もSendを満たすことができない。

コンパイラにお任せできないこともある。メソッドをスレッド安全でない方法で実装したときなどがこれに該当する。この場合はimpl !Send for Tみたいにして明示的にSendを実装しない、と表明する必要がある。

thread::spawn()に話を戻す。thread::spawn()はクロージャを受け取る。API定義はそのクロージャがSendを満たしていないといけない、と言っている。ではクロージャがSendかどうかはどう決まるのか。それはクロージャの作り方によって決まる。クロージャがSendでない値をキャプチャしていたら、そのクロージャはSendではない。ただし、Sendではない値をクロージャの内部で使っていても、クロージャ自体はSendになることができる。

// Error
fn f1() {
    let v = Rc::new(42);
    thread::spawn(move || {
        let _v2 = Rc::clone(&v);
    });
}

// OK
fn f2() {
    thread::spawn(|| {
        let _v = Rc::new(42);
    })
}

API定義でSendを見かけたときの心構えとしてはどう考えればいいか。ほとんどの型がSendであることを考えれば、スレッド越しに値を渡すよということぐらいで特に気にしなくても良さそう、というのが今の理解。

余談だけど、スレッド安全に関するもう一つのトレイトとしてSyncがある。こちらはthread::spawn()と直接関係がなかったのであまり追っておらず、理解があやふやなので言及を避けた。