Arc

不変の連結リストを使う理由として,スレッド間でデータを共有したいというものがあります. とどのつまり,諸悪の根源は共有された可変な参照なので,この問題を解決する方法として 可変なというところを永久に葬り去ってしまうという手があります.

私達のリストはこれっぽっちもスレッドセーフではありません.スレッドセーフにしたければ 参照カウントをアトミックにする必要があります.そうでなければ2つのスレッドが同時に 参照カウントをインクリメントしようとし,片方だけが通ることがあり得ます.そうなれば リストはまだ参照されているノードのメモリ割り当てを解放してしまうでしょう!

スレッドセーフにするためにはArcを使う必要があります.Arcはたった一つの特徴, 参照カウントがアトミックに変更される点を除いてRcと完全に同じものです.Rcで十分な場合には これは若干の余分なオーバーヘッドを伴うので,ArcとRcのどちらも使えるようになっています. 私達がすべきことはRcをstd::sync::Arcに変えるだけです.はい,もうスレッドセーフです. 終わり!

しかし,ひとつ面白い疑問が湧いてきます.どうすればある型がスレッドセーフかどうか 知ることができるでしょうか?私達は何かやらかしてしまったのでしょうか?

いいえ!Rustのスレッド安全性を脅かすことはできません!

なぜなら,RustはSendSyncという2つのトレイトを使って,スレッド安全性を 第一級の機能としてモデル化しているからです.

ある型は他のスレッドに安全にムーブできるときSendを持ちます.また,複数のスレッド間で 共有できるときSyncを持ちます.つまりTがSyncのとき&TはSendです.ここで安全と 言っているのはデータ競合を防げるという意味です(競合状態という,さらに一般的な 問題と取り違えないでください).

この2つはマーカートレイトです.つまりインターフェースの実装はありません.ある型は Sendであるかそうでないかの二択です.ほかのAPIが,ある型がSendであるかチェックしたり して使われるものです.そしてSendでない型なら他のスレッドに送ることができなくなります! すごい!

また,SendとSyncは型のフィールドが全てSendやSyncであるかどうかによって自動的に 付与されます.Copyのときと似ていますが,自分で付与することもできる点が違います.

ほぼ全ての型はSendでありSyncです.たいていの型は自分自身のデータを所有しており, したがってSendです.また,たいていの型をスレッド間で共有する方法は共有参照を 取って不変にすることだけであり,したがってSyncです.

しかし,なかには内部可変性という特徴を持ち,これらの条件に当てはまらない型があります. ここまで私達が見てきた可変性は継承可変性(もしくは外部可変性)とよばれるもので, 値の可変性がその入れ物から継承されているものでした.継承可変性を持つ型は,不変な値の フィールドをなんとなく変更したりすることはできません.

内部可変性は違います.共有参照の中にあるものを変更できるのです.内部可変性はだいたい 2つに分けられます.単一スレッドでのみ動作するCellと,複数スレッドで動作するLockです. Cellのほうが低コストで使えることは明らかですね.あとはLockと似た動作をするプリミティブ型, atomicがあります.

で,こいつらがどうRcやArcと関係するのでしょうか?実はRcもArcも参照カウントのために 内部可変性を持っているのです.更に悪いことに参照カウントは全てのインスタンス間で 共有されているのです!RcはCellをつかっており,したがってスレッドセーフではありません. Arcはatomicを使っているのでスレッドセーフです.とはいえ,もちろんArcの中に型を 突っ込むことでスレッドセーフにするようなことはできず,他の型と同じようにしか スレッド安全性を得られません.

私はぜっっっっったいにatomicのメモリ設計やSendを自分で実装することについて話したく ありません.言うまでもないことですが,Rustのスレッド安全性の話に入り込むほど話は 複雑になっていきます.Rustを普通に使う人は,そう動くもんだと思っていればこういった ことについて考える必要はありません.