整数をインクリメントするスレッド vs. その整数を表示するスレッド
1. 動機
以下のプログラムを考える:
メインスレッドで共有の整数nを用意する(初期値は0)。そして以下の2つのスレッドを立ち上げる。
・1000回だけnを標準出力に書き出す。
・1000回だけnをインクリメントする。
前者のスレッドは出力行を書式設定して書き出さなければならない(つまり、後者のスレッドに比べて多くの機械命令を必要とする)ので、前者のスレッドが表示するnの値はすぐに1000になるはずである。それを手を動かして確認したくなった。言語はRustを使用。
2. ロック等を使わず各々のスレッドが同期なしで動くとき
タイトルの通りのものをRustで書いた。Rustみたいに言語レベルで並行プログラミングの安全性を保障してる言語だと、簡単な並行プログラムなら同期なしで書くほうがかえって面倒だ。
use std::thread; use std::ptr::read; fn main() { let mut n = 0; let p = UnsafePtr::new(&mut n); let x = thread::spawn(move || { for _ in 0..1000 { println!("The value of n is {}.",unsafe {read(p.0) }); } }); let y = thread::spawn(move || { for _ in 0..1000 { p.inc(); } }); let _ = x.join(); let _ = y.join(); } #[derive(Copy,Clone)] struct UnsafePtr(*mut usize); impl UnsafePtr { fn new(n: &mut usize) -> Self { Self(n as *mut usize) } fn inc(self) { let old = unsafe {self.0.read()}; unsafe {self.0.write(old+1)}; } } unsafe impl Sync for UnsafePtr {} unsafe impl Send for UnsafePtr {}
上記のプログラムを3回実行した。
実行結果その1
The value of n is 100. The value of n is 1000. The value of n is 1000. The value of n is 1000. The value of n is 1000. The value of n is 1000. . . . The value of n is 1000.
実行結果その2
The value of n is 187. The value of n is 972. The value of n is 1000. The value of n is 1000. The value of n is 1000. The value of n is 1000. . . . The value of n is 1000.
実行結果その3
The value of n is 0. The value of n is 342. The value of n is 380. The value of n is 405. The value of n is 429. The value of n is 452. The value of n is 474. The value of n is 497. The value of n is 520. The value of n is 541. The value of n is 563. The value of n is 586. The value of n is 607. The value of n is 630. The value of n is 653. The value of n is 674. The value of n is 696. The value of n is 718. The value of n is 739. The value of n is 761. The value of n is 782. The value of n is 804. The value of n is 826. The value of n is 848. The value of n is 870. The value of n is 891. The value of n is 913. The value of n is 935. The value of n is 957. The value of n is 980. The value of n is 1000. The value of n is 1000. The value of n is 1000. . . . The value of n is 1000.
上述の通り、0から1000まで均等に表示されるのではなく、すぐに1000までインクリメントされる。
3. ロックを使って各々のスレッドが排他的にnにアクセスして動くとき
今度はロックを使って各々のスレッドは排他的にnにアクセスするようにする。
use std::{sync::{Arc,Mutex}, thread}; fn main() { let n = Arc::new(Mutex::new(0)); let a = n.clone(); let b = n.clone(); let x = thread::spawn(move || { for _ in 0..1000 { println!("The value of n is {}.",a.lock().unwrap()); } }); let y = thread::spawn(move || { for _ in 0..1000 { *b.lock().unwrap() += 1; } }); let _ = x.join(); let _ = y.join(); }
上記のプログラムを3回実行した。
実行結果は・・・nの値が1000になるのは大体500行目以降となったので、それを載せると無駄に記事が長くなってしまうのでやらない。ともかく、nの増加の勾配は2. のときに比べて緩やかになった。これは、println!マクロを実行している間はスレッドxがロックを取得しているため、スレッドyはprintln!が完了するまでnにアクセスすることが出来ないからだと思う。
4. Haskellでやってみる
ついでにHaskellでもやってみた。Haskellで2.と同等のプログラムの書き方が分からない(この辺使ったらできるのかな)ので、3. と同等のプログラムを書いてみた。
import Control.Concurrent.Async import Control.Monad import Control.Concurrent import Text.Printf main :: IO () main = do m <- newMVar (0 :: Int) x <- async . replicateM_ 1000 $ do n <- takeMVar m printf "The value of n is %d.\n" n putMVar m n y <- async . replicateM_ 1000 $ do n <- takeMVar m putMVar m (n+1) wait x wait y
実行結果はこちら
The value of n is 0. The value of n is 1. The value of n is 2. The value of n is 3. The value of n is 4. The value of n is 5. The value of n is 6. The value of n is 7. The value of n is 8. The value of n is 9. The value of n is 10. The value of n is 11. The value of n is 12. The value of n is 13. . . . The value of n is 994. The value of n is 995. The value of n is 996. The value of n is 997. The value of n is 998. The value of n is 999.
あれ・・・ものすごく綺麗にインクリメントしてる(交互にスレッドが実行されている)。10回ほど実行しても毎回上記の結果となった。理由がわかったら追記します。