[Rust] マーカトレイトから見る言語仕様
はじめに
本記事はRust Advent Calendar 2021の22日目の記事です。
この記事では std::marker に含まれるマーカトレイトを実装しているかいないかで
- ArcとかCell等のスマートポインタ使い分け
- CloneとCopyの使い分け
などの違いを説明することを目指します。説明を簡略化するために、ある型TがトレイトAを実装する場合、TはAであるといいます。
マーカートレイト
マーカトレイトはコンパイラの動作を決定するのに使われ、コンパイラから特別扱いされているトレイトです。空のトレイトなので、Copyであれば次のように実装することができます
impl Copy for MyStruct {}
これらのマーカートレイトはstd::markerに含まれています。ここではSync,Send,Sized,Copy を扱います。
https://doc.rust-lang.org/core/marker/index.html
Sizedトレイト
Rustにおける変数束縛に関わるマーカートレイトとしてstd::marker::Sizedがあります。
pub trait Sized {}
もし、Sizedを実装されていない場合
- 変数に束縛できず、関数の引数や戻り値にできない。
- 型への参照はfat pointerになる。
参照やほとんどの型には暗黙のうちに実装されています。実装していない型をDST(Dynamic Sized Type)といい、例えば次があります。
- トレイトオブジェクト
- str
- スライス
これらの型はSizedでないため、プログラム内ではfat pointerを通して扱います。fat pointerのメモリレイアウトは
- ポインタ(8byte)
- 追加情報(8byte)
追加情報はスライスなら要素数、strならバイト数など、サイズを決定するために必要なデータが含まれます。Sizedは手動で実装ができず、DST以外の型には自動で実装されます。
Copyトレイト
Rustにおけるムーブセマンティクスを制御するマーカトレイトとしてstd::marker::Copyがあります。
Copyである型はムーブセマンティクスの代わりにコピーセマンティクスが発生します。ある型TがCopyであるには以下の条件を満たしている必要があります。
-
Cloneである。 - 単純なバイトコピーが可能である。
- 型Tが構造体や列挙型なら、そのフィールドが全て
Copyを実装している。
Cloneがただのバイトコピーとして実装される場合に限り、Copyです。例えば、ArcはCloneするときに参照カウンタをインクリメントする必要があるので単純なバイトコピーができません。
ここを参考にすると、ムーブについては次のような3つに分類できます。
- 値がムーブする
linear - 値がムーブするが
CloneすることができるCloneable - 値がムーブするときにコピーされる
Copyable
| 特性 | semantics | Clone | Copy | バイトコピー |
|---|---|---|---|---|
| Linear | Move | x | x | 不可 |
| Cloneable | Move | ○ | x | 不可 |
| Copyable | Copy | ○ | ○ | 可 |
Linearというの線形型システムから来ていると思われます。
Send・Syncトレイト
std::marker::Sendとstd::marker::Syncは並列処理の安全性を保証するのに使われるマーカトレイトです。
この二つである型は次のような性質を持ちます。
- Send 所有権を別スレッドに移動できる型。
- Sync 参照(&T)を別スレッドに移動できる型。型Tの参照がSendである場合にのみ、Syncです。
ほとんどの型はデフォルトでSendとなっています。生ポインタはSendでもSyncでもありません。もし生ポインタのようなSendやSyncでもない型に実装したい場合、unsafe implでコンパイラに対してスレッドセーフであることを明示することになります。
unsafe impl Send for MyStruct {}
SendやSyncでない型を複数スレッドで共有したい場合、次のスマートポインタが利用できます。
-
Sendにしたい場合Arcに包む -
Syncにしたい場合Mutex、RwLockに包む。
例えばArc<Mutex<T>> での利用例を見てみましょう。MutexはSend、Syncを実装しているので、参照を他のスレッドに移すことができます。しかし、Mutexは Cloneを実装していないので、他のスレッドに移した後に元のスレッドから参照することができず、複数のスレッドからアクセスすることができなくなります。例えば次のコードでは、xがmoveされてしまうのでエラーになります。
use std::sync::Mutex;
use std::thread
fn main() {
let x = Mutex::new(2);
let child = thread::spawn(move|| {
dbg!(x);
});
child.join().unwrap();
dbg!(x); // error[E0382]: use of moved value: `x`
}
この場合、Sendを実装していてかつCloneができるArcで包む方法が有効です。Arc<Mutex<T>>は所有権を複数スレッドで共有しながら値を変更することが可能になります。
https://doc.rust-lang.org/std/sync/struct.Arc.html
RcやRefCellは逆にSyncやSendを実装しておらず、複数スレッドからの書き換えや所有権の移動ができません。
-
RcはArcと違い参照カウンタがアトミックではないので、カウント中に割り込まれる可能性がある。そのため、他のスレッドに所有権を移すと、データ競合が生じる可能性があり、SendではなくSyncでもない。 -
RefCellの借用チェックはスレッドセーフではない。他のスレッドに所有権を移すと、元のスレッドからのアクセス権が失われてデータ競合が生じないので、SendだがSyncではない。
ここまで登場したRustのスマートポインタの実装しているトレイトまとめると次のようになる。
| Send | Sync | Clone | Copy | |
|---|---|---|---|---|
| Rc | x | x | o | x |
| Arc | o | x | o | x |
| Cell | o | x | o | o |
| RefCell | o | x | o | x |
| Mutex | o | o | x | x |
まとめ
Rustにおいては所有権が移動したことやスレッドセーフでないことをコンパイラが検知することができます。自動実装されるマーカートレイトを参考にコンパイラがスレッドセーフを保証してくれるRustの型システムの恩恵を感じます。
記事内容に誤りがあった場合、コメントで教えていただけると嬉しいです。
参考
Discussion
私もつい最近まで勘違いしていましたが、これは間違いです。SyncやSendでない型をArcやMutexで包んでもSyncやSendにはなりません。
Arc<T>のTは確かになんでもいいのですが、TがSync + Sendの時のみArc<T>もSyncかつSendになります。これはArcのAPI docを見るとわかります。
MutexについてはTがSendの時にMutex<T>がSendかつSyncになります。
ArcとMutexはあくまでマルチスレッド環境でRcやRefCellの機能を実現したいときに使用するというだけで、型TをSyncやSendにするものではありません。