疑問: 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>
は、T
がSend
であれば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()
と直接関係がなかったのであまり追っておらず、理解があやふやなので言及を避けた。