[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にするものではありません。