今回のお題はこのコードです。
fn main() {
// スレッド間で安全に使えるようにする
let counter = Arc::new(Mutex::new(0));
// スレッドを建てる
let threads = (0..2).map(|_| {
let counter = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..5 {
// 10msごとに実行する
thread::sleep(Duration::from_millis(10));
let mut counter = counter.lock().unwrap();
*counter += 1;
}
});
// 5ms秒ごとにスレッドを建てる
thread::sleep(Duration::from_millis(5));
handle
}).collect::<Vec<_>>();
// すべてのスレッドを待つ
for thread in threads {
thread.join();
}
// 実行結果を表示
println!("{}", *counter.lock().unwrap());
}
特に問題なく実行できるコードです。
このコードは共有カウンターcounterを、二つのスレッドで1スレッド5回、合計10回インクリメントします。
その結果を出力します。
尚、インクリメント時にスリープ処理を入れてます。 その理由は後述します。
とりあえずコード解説を
前提知識を少しだけ説明します。
所有権&参照
一般的に、数値や文字列と行った 「値」 は、変数と呼ばれる 「文字」 に関連づけられます。
入門書では、よく箱にたとえられます。
この箱はメモリに保管されます
ところで、ルールを決めないで変数を扱うとどうなるでしょうか?
次のことが考えられます
- 想定していた値と、異なるタイプだったりする
- 値がまだない可能性すらある
- 他のスレッドからポインタや参照を用いて値が意図せず書き換えられる
- 削除するルールが存在しないため、値が永遠に蓄積されていく
- 値が誰かによって削除される可能性すらある
これは恐ろしいことです。
そのため、1 と 2 は型システムと null 安全性によって保護されています。
rustでは言語仕様によって nullで変数を初期化することはできないことを保証してくれます。
let x = null; // NG! コンパイルエラー!
3 は基本が不変であることが解決します。 mutキーワードをつけない変数は不変であり、値が変わることはありません。
let x = 3;
x = 10; // NG! コンパイルエラー!
代わりに、シャドーイングが許可されてます。 これは、一度変数に使った文字を別の変数としてもう一度束縛使えることです。
それぞれ別の変数となるため、型が変わっていても 問題ありません。
let x = 3;
let x = x + 2; // 手前のxと、このxの値は別物
4,5 は所有権が解決します。
所有権とは値の所有権を指します。 その値を束縛できる変数が一つであることを示します。
言い換えればこれは、ポインタや参照を安易に作れないことを意味します。
所有権はスコープ内で有効です。 スコープを外れた場合は 4 を解決するために値は削除されます。
また、rust の変数はCopyトレイトが実施されているi32などを除き move です。
move とは、所有権の移動です。 比較的簡単に move ができます。
let x = "hello world"; // 所有権発動
{
// スコープの中にxが移る (move)
let x = x;
println!("{}", x);
// xと、その値はここで削除される
}
println!("{}", x); // NG! コンパイルエラー!
参照を使えばそれを解決できます。 move を行わないです。
let x = "hello world"; // 所有権発動
{
// スコープの中にxが移る (move)
let x = &x;
println!("{}", x);
// xと、その値はここで削除される
}
println!("{}", x); // hello world
参照の注意として、所有者が消えたら参照も無効になることです。
所有者の生存時間<参照の生存時間 であれば コンパイル時エラーが発生します。
コンパイル時エラー、つまりコンパイルできないということです。
コンパイルエラー、つまり
危ないソフトウェアは生成できない!
Arc
これは参照カウンタと呼ばれるものです。Rc というのもありますが、Arcとはスレッドセーフの有無で異なります。マルチスレッドで動かすので、今回はこちらを使います。
参照カウンタとは、通常の参照と異なり、参照のライフタイムで値のライフタイムを決める仕組みです。
つまり、最後の参照が消えたタイミングで、値を消去します。
通常のライフタイムは所有者のライフタイムで決まるのでその点違います。
シングルスレッドと異なり、並行的に処理が動作するマルチスレッドでは、スレッド間で共有する変数のライフタイムの計算ができません。
シングルスレッドは上から下へ、ループや関数でソースコードを飛ぶことはあれど基本は上から順番に実行されます。 追い抜かすことはありません。
let x = 1;
assert!(x==1);
let x = x + 1;
assert!(x==2);
let x = x / 2;
assert!(x==1);
let mut x = x * 5;
assert!(x==5);
for _ in 0..5 {
x = x - 1;
}
assert!(x==0);
一方、マルチスレッドはこれを追い越す可能性があるのです。 つまり、1,2,3 という順番でプログラミングしていたのが、
実際は 1,3,2 の順で処理が進むなんてことがあるのです。
よって最後に変数を使うタイミングをコンパイル時に求めることができないため、マルチスレッド間で共有する変数には、別の仕組みを用意する必要があったのです。
Mutex
Mutex は、値の独占権を得られるものです。
use std::{sync::{Arc, Mutex}, thread, time::Duration};
fn main(){
let x = Arc::new(Mutex::new("helloworld"));
// スレッドを建てる
{
let x = x.clone();
thread::spawn(move || {
// 0.5秒lockしておく
let locking = x.lock().unwrap();
thread::sleep(Duration::from_millis(2000));
});
}
// 再びメインスレッドへ
// lockが取得できるまで待たされる。
println!("{}", x.lock().unwrap());
}
多分実行したら 2 秒ほど奪われると思います。
変数lockingは、mutexの.lock()メソッドにより、値の独占権をもらいます。
この時点ですでに独占されている場合は、独占権を得られるまでブロックされます!
このおかげで、3. 他のスレッドからポインタや参照を用いて値が意図せず書き換えられるを 解決できます
一つの災
とりあえず何故かクソ丁寧に rust の前提知識を説明しましたが、足りなかったらぜひThe Rust Programming Languageをお読みください。
さて、この項目では最初に出したコードにある落とし穴の説明をします。
// スレッド間で安全に使えるようにする
let counter = Arc::new(Mutex::new(0));
// スレッドを建てる
let threads = (0..2).map(|_| {
let counter = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..5 {
// 10msごとに実行する
let mut counter = counter.lock().unwrap();
thread::sleep(Duration::from_millis(10));
*counter += 1;
}
});
// 5ms秒ごとにスレッドを建てる
thread::sleep(Duration::from_millis(5));
handle
}).collect::<Vec<_>>();
// すべてのスレッドを待つ
for thread in threads {
thread.join();
}
// 実行結果を表示
println!("{}", *counter.lock().unwrap());
2 行ほどコードが変わりました。 どこが変わったら考えてみてください。
また、これにより実行時間が変わってます。 計測してみましょう。
最初にあげたコードの場合
とりあえず計測方法は雑です。 差があれば説明が付くので。
なお、先ほどまでのコードはfunc()に収めてます。
use std::{sync::{Arc, Mutex}, thread, time::{Duration, SystemTime}};
fn main() {
let now = SystemTime::now();
func();
println!("time: {}", now.elapsed().unwrap().as_millis());
}
fn func() {
// スレッド間で安全に使えるようにする
let counter = Arc::new(Mutex::new(0));
// .....
// スレッドを建てる
let threads = (0..2).map(|_| {
let counter = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..5 {
// 10msごとに実行する
thread::sleep(Duration::from_millis(10));
let mut counter = counter.lock().unwrap();
*counter += 1;
}
});
// 5ms秒ごとにスレッドを建てる
thread::sleep(Duration::from_millis(5));
handle
}).collect::<Vec<_>>();
// すべてのスレッドを待つ
for thread in threads {
thread.join();
}
// 実行結果を表示
println!("{}", *counter.lock().unwrap());
}
一つの災のコード
use std::{sync::{Arc, Mutex}, thread, time::{Duration, SystemTime}};
fn main() {
let now = SystemTime::now();
func();
println!("time: {}", now.elapsed().unwrap().as_millis());
}
fn func() {
// スレッド間で安全に使えるようにする
let counter = Arc::new(Mutex::new(0));
// .....
// スレッドを建てる
let threads = (0..2).map(|_| {
let counter = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..5 {
let mut counter = counter.lock().unwrap();
// 10msごとに実行する
thread::sleep(Duration::from_millis(10));
*counter += 1;
}
});
// 5ms秒ごとにスレッドを建てる
thread::sleep(Duration::from_millis(5));
handle
}).collect::<Vec<_>>();
// すべてのスレッドを待つ
for thread in threads {
thread.join();
}
// 実行結果を表示
println!("{}", *counter.lock().unwrap());
}
結果
time が前者のコードは 55、後者のコードは 100 となりました。(
環境によって伸びる可能性あり
何故このようなことが起きるのでしょうか?
そもそもどこが異なっていたのでしょうか? その答えは...
// スレッド間で安全に使えるようにする
let counter = Arc::new(Mutex::new(0));
// .....
// スレッドを建てる
let threads = (0..2).map(|_| {
let counter = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..5 {
+ let mut counter = counter.lock().unwrap();
// 10msごとに実行する
thread::sleep(Duration::from_millis(10));
- let mut counter = counter.lock().unwrap();
*counter += 1;
}
});
// 5ms秒ごとにスレッドを建てる
thread::sleep(Duration::from_millis(5));
handle
}).collect::<Vec<_>>();
// すべてのスレッドを待つ
for thread in threads {
thread.join();
}
// 実行結果を表示
println!("{}", *counter.lock().unwrap());
たったこれだけです。 lock する時間に 10ms という空白も含まれてしまっていたのです。
これだけだとまだ簡単だけど...
周辺のコードだけを抜粋しました。
for _ in 0..5 {
let mut counter = counter.lock().unwrap();
// 10msごとに実行する
thread::sleep(Duration::from_millis(10));
*counter += 1;
}
ちょっと改善してみました。
for _ in 0..5 {
let mut counter = counter.lock().unwrap();
*counter += 1;
// 10msごとに実行する
thread::sleep(Duration::from_millis(10));
}
うーん結果は 100 ミリ秒か。 変わんないな。 それもそうです。
このコードではライフタイムを考慮する必要があります。 lock は所有権のライフタイムと一致しており
所有権が切れるのはスコープを抜けるまでです
for _ in 0..5 {
{
let mut counter = counter.lock().unwrap();
*counter += 1;
}
// 10msごとに実行する
thread::sleep(Duration::from_millis(10));
}
このコードの結果は 55 秒です。
スコープを分けたので、sleep 前に lock の所有権が切れてしまうからです。
どうしてこんなことが起きるの
rustでは、オブジェクトが消える際に呼び出されるDropトレイトがあります。
このトレイトではオブジェクトが消える時に処理を走らせることができます。
mutexではこの仕組み応用しているわけです。
まとめ
所有権の仕組みは面白いと思います。
この記事を書いた当初は、所有権に関するトラブルが多かったです。
この記事のように、mutexにまつわる問題だけでなく、TCPListenerなどではクライアントにレスポンスを返せないトラブルもありました。
もしそういったトラブルにハマった場合は、変数のライフタイムにも注目するといいかもしれません。
感想、指摘等あればコメント欄にて気軽にお願い致します!
(※当記事は自サイトで投稿していた内容を改良し、qiitaにて持ってきたものになります)