Open5

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

sankantsusankantsu

Send と Sync

  • Send: TSend ⇔ 所有権を他のスレッドに移動できる
  • Sync: TSync&TSend (スレッド間で参照を共有できる)

Automatic implementation

普通、 SendSync は自動で実装される

  • 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

https://doc.rust-lang.org/nomicon/send-and-sync.html

sankantsusankantsu

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> 内部のオブジェクトを複製できる。

TCopy であれば基本的には 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

https://doc.rust-lang.org/std/cell/
https://doc.rust-lang.org/reference/interior-mutability.html
https://qiita.com/mosh/items/c7d20811df218bb3188e

sankantsusankantsu

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> が用いられている。

関連記事

https://zenn.dev/tan_y/articles/307533011ac2c0

sankantsusankantsu

Send を実装してよい条件は形式的に定義できるだろうか?
Informal には例えば以下の記述がある (Rustnomicon):

A type is Send if it is safe to send it to another thread.

しかし "safe to send it to another thread" がいまいち抽象的で、何をもって判断してよいかがよくわからない。

sankantsusankantsu

Rc の反例からすると、 Rust の型システム上の所有権と、抽象的な意味での所有権が一致せず、型システム上の所有権の移譲が完全には所有権を移動しないような場合に Send が満たされなくなるように思われる。
それでは、ここでいう「抽象的な所有権」は実際上どのように定義できるのであろうか?

所有権と可変参照の保持の関係性も、実のところ正確に理解できていないような気もしてくる。

オブジェクト、参照、変数、ライフタイム、所有権の関係は整理してみたいような気がする。