データ競合と競合状態
安全な Rust では、データ競合が存在しないことが保証されています。 データ競合は、以下のように定義されています。
- 2 つ以上のスレッドが並行にメモリ上の場所にアクセスしている
- この内 1 つは書き込み
- この内 1 つは非同期
データ競合は未定義動作を含み、そしてそれ故に安全な Rust で発生させることは不可能です。 データ競合は Rust の所有権システムによってほとんど防がれています。可変参照の エイリアスを生成することは不可能ですから、データ競合を起こすことは不可能です。 内部可変性はこれをもっと複雑にします。これが、 Send トレイトと Sync トレイトが 何故存在するかということの主な理由です (以下を見てください) 。
しかしながら Rust は、一般的な競合状態を防ぎません。
これは根本的に不可能で、そして多分本当に望まれていないものです。ハードウェアは 競合状態を起こし、 OS も競合状態を起こし、コンピュータの他のプログラムも競合状態を起こし、 そして世界中にある全てのプログラムも競合状態を起こします。どんなシステムでも、 全ての競合状態を防げると喧伝しているようなものは、単に間違っているだけではなく、 本当に使いづらいものとなるでしょう。
ですから、安全な Rust のプログラムがデッドロックに陥ったり、正しくない同期によって何か 馬鹿げたことを行なっても、これは全く "問題ない" のです。明らかにそのようなプログラムは、 本当に良くないです。ですが、 Rust は今までのところ、プログラマに我慢してもらうしか出来ないのです。 それでも Rust のプログラムだけでは、競合状態において、メモリ安全性を侵害することは出来ません。 何か他のアンセーフなコードと組み合わせることだけでしか、実際に競合状態において、 メモリ安全性を侵害することが出来ないのです。例:
#![allow(unused)] fn main() { use std::thread; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; let data = vec![1, 2, 3, 4]; // Arc にすることで、 他のスレッドより前に完全に実行が終了しても、 AtomicUsize が // 保存されているメモリが、他のスレッドがインクリメントするために存在し続けます。 // これ無しにはコンパイルできません。なぜなら、 thread::spawn が // ライフタイムを必要とするからです! let idx = Arc::new(AtomicUsize::new(0)); let other_idx = idx.clone(); // `move` によって other_idx が値でキャプチャされ、このスレッドにムーブされます thread::spawn(move || { // idx を変更しても大丈夫です。この値はアトミックだからです。 // ですからデータ競合は起こりません。 other_idx.fetch_add(10, Ordering::SeqCst); }); // アトミックなものからロードした値を使用してインデックス指定をします。これは安全です。 // なぜなら、アトミックメモリから読み込み、その値のコピーを Vec のインデックス実装に // 渡すからです。このインデックス指定では、正しく境界チェックが行なわれ、そして途中で // 値が変わることはありません。しかし、もしスポーンされたスレッドが、なんとかして実行前に // インクリメントするならば、このプログラムはパニックするかもしれません。 // 正しいプログラムの実行 (パニックすることはほとんど正しくありません) は、スレッドの // 実行順序に依存するため、競合状態となります。 println!("{}", data[idx.load(Ordering::SeqCst)]); }
#![allow(unused)] fn main() { use std::thread; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; let data = vec![1, 2, 3, 4]; let idx = Arc::new(AtomicUsize::new(0)); let other_idx = idx.clone(); // `move` によって other_idx が値でキャプチャされ、このスレッドにムーブされます thread::spawn(move || { // idx を変更しても大丈夫です。この値はアトミックだからです。 // ですからデータ競合起こりません。 other_idx.fetch_add(10, Ordering::SeqCst); }); if idx.load(Ordering::SeqCst) < data.len() { unsafe { // 境界チェックを行なった後、間違えて idx をロードしてしまいます。 // この値は変わってしまったかもしれません。これは競合状態で、*危険*です。 // なぜなら `unsafe` である `get_unchecked` を行なったからです。 println!("{}", data.get_unchecked(idx.load(Ordering::SeqCst))); } } }