Rust 並行性 (Send, Sync) と cell のメモ

Send と Sync
-
Send
:T
がSend
⇔ 所有権を他のスレッドに移動できる -
Sync
:T
がSync
⇔&T
がSend
(スレッド間で参照を共有できる)
Automatic implementation
普通、 Send
や Sync
は自動で実装される
-
T
の メンバがすべてSend
->Send
-
T
のメンバがすべてSync
->Sync
Send
または Sync
ではない型
多くの premitive および、その合成でつくられる型は基本的に Send
かつ Sync
以下は Send
ではないまたは Sync
ではない型の例
- Raw pointer は
Send
でもSync
でもない -
UnsafeCell<T>
(および他の cell) はSend
だがSync
ではない -
Rc<T>
はSend
でもSync
でもない - (マイナーな例)
MutexGuard<'_, T>
はSync
だがSend
ではない (doc)
Cell について
UnsafeCell<T>
は interior mutability を導入する型であるから、共有参照をスレッド間で共有することが安全ではなくなる -> Sync
ではない。
ただし、UnsafeCell<T>
自体への所有は共有されないことが型システムで保証されるから、 Send
は保たれる。
Rc について
(以下は自己解釈)
Rc<T>
は 1 つのオブジェクト (T
) への所有権を複数の変数で共有するものだから、変数を別のスレッドに move しても所有権がもとのスレッドにも残っている。
したがって、もし Rc<T>
が Send
を実装していたとすると複数のスレッド間で所有権を共有することになる。
しかし、 Rc<T>
は所有権の管理を atomic ではないカウンタ (Cell<usize>
(src)) で管理しているから、管理が複数スレッドにまたがるとカウンタに対するデータ競合を起こす。
したがって、 Rc<T>
は Send
ではない。
Rc::clone()
は &Rc<T>
から T
への所有権をもつ変数を作成することができるから、 Rc<T>
は Sync
も満たさない。
Reference

Cell について
Rust の cell 型は、 interior mutability を導入するための型である。
通常、共有参照 (&T
) からはオブジェクトは変更されない (それを仮定して最適化して良い)。
この仮定を外し、共有参照経由でオブジェクトの変更を可能にすることを interior mutability を与えるという。
Interior mutability があるからといって、その型自体への参照の個数に関するチェックが無効になるわけではない (e.g. ひとつのオブジェクト UnsafeCell<T>
への複数の &mut UnsafeCell<T>
が存在できるわけではない)
std::cell
は interior mutability を持つような型をライブラリとして提供する。
UnsafeCell
UnsafeCell
は Rust 言語として唯一特別に interior mutability を持つ型である (doc)。
すなわち、共有参照 (&UnsafeCell<T>
) 型から内部のオブジェクト T
に変更を与えることができる。
get()
を通して &UnsafeCell<T>
から内部のオブジェクトへの *mut T
を取得できる。
*mut T
を用いて変更を安全に行うのはプログラマの責任である。
たとえば、 x: UnsafeCell<T>
に対して unsafe {&mut *x.get()}
によって &mut T
を取得できる。
プログラマはこのようにして取得した &mut T
が複数存在しないように管理する必要がある。
実装上は単に T
型のオブジェクトを wrap している (src)。ただし、 #[lang = "unsafe_cell"]
という attribute がついており、これが言語的に特別扱いされることを表していることになりそう。
UnsafeCell<T>
は言語仕様的には重要な型であるが、基本 UnsafeCell<T>
を直接使うことは少なくて、 Cell<T>
や RefCell<T>
を使うことになりそう。
Cell
Cell<T>
は UnsafeCell<T>
を wrap した型で、 T
型の値を出し入れすることで中身を変更することができる。
.set()
や .replace()
を通して共有参照 &Cell<T>
からオブジェクトを変更できる。
T: Copy
であれば、 .get()
で Cell<T>
内部のオブジェクトを複製できる。
T
が Copy
であれば基本的には RefCell<T>
よりオーバーヘッドが少ない Cell<T>
を使うのが良いようである。
RefCell
RefCell<T>
は &T
および &mut T
を共有参照 &RefCell<T>
から取得することを可能にする型である。
参照の数は実行時にカウンタ (Cell<isize>
) によって管理され、 &mut T
とほかの参照が同時に発生しないように実行時チェックで保証される。
このカウンタは atomic ではないので、 thread safe ではない。
&T
を取得したい場合は borrow()
, &mut T
を取得したい場合は borrow_mut()
を利用する。
Reference

Rc と Cell
Rc は所有権を共有することを可能にするが、 Rc<T>
から &mut T
を取得することはできないので、 T
に変更を与えることができない。
一方、 Deref trait の実装から &T
を取得することは可能である。
もし Rc<T>
を用いる場合に T
に変更を加える必要があれば、共有参照 &T
から変更を行う必要がある。
これは、 RefCell<T>
のような interiror mutability を用いて実現可能である。
このような事情から、 Rc<RefCell<T>>
は「所有権を共有し、かつ安全に変更可能にする」ための idiom として慣用的に用いられる。
また、 Rc<T>
自身も共有カウンタを内部実装としてもつ必要があり、これには Cell<usize>
が用いられている。
関連記事

Send
を実装してよい条件は形式的に定義できるだろうか?
Informal には例えば以下の記述がある (Rustnomicon):
A type is Send if it is safe to send it to another thread.
しかし "safe to send it to another thread" がいまいち抽象的で、何をもって判断してよいかがよくわからない。

Rc の反例からすると、 Rust の型システム上の所有権と、抽象的な意味での所有権が一致せず、型システム上の所有権の移譲が完全には所有権を移動しないような場合に Send
が満たされなくなるように思われる。
それでは、ここでいう「抽象的な所有権」は実際上どのように定義できるのであろうか?
所有権と可変参照の保持の関係性も、実のところ正確に理解できていないような気もしてくる。
オブジェクト、参照、変数、ライフタイム、所有権の関係は整理してみたいような気がする。