Go言語と Rust で Mutex による排他制御
以下の 3通りを Go 言語と Rust でそれぞれ実装してみました。
サンプルコードは http://github.com/fits/try_samples/tree/master/blog/20201122/
Go 言語の場合
まずは Go 言語による実装です。 Go 言語では goroutine で軽量スレッドによる並行処理を実施できます。
ここでは、goroutine の終了を待機するために sync.WaitGroup
を使いました。
WaitGroup では Add
した数に相当する Done
が呼び出されるまで Wait
でブロックして待機する事が可能です。
go_mutex_sample.go
package main import ( "fmt" "sync" ) type Data struct { value int } // (a) func noLock() { var wg sync.WaitGroup var ds []Data for i := 0; i < 100; i++ { wg.Add(1) go func() { ds = append(ds, Data{i}) wg.Done() }() } // goroutine の終了を待機 wg.Wait() fmt.Println("(a) noLock length =", len(ds)) } // (b) func useMutex() { var wg sync.WaitGroup var mu sync.Mutex var ds []Data for i := 0; i < 100; i++ { wg.Add(1) go func() { mu.Lock() ds = append(ds, Data{i}) mu.Unlock() wg.Done() }() } wg.Wait() fmt.Println("(b) useMutex length =", len(ds)) } // (c) func useRWMutex() { var wg sync.WaitGroup var mu sync.RWMutex var ds []Data for i := 0; i < 100; i++ { wg.Add(1) go func() { mu.Lock() ds = append(ds, Data{i}) mu.Unlock() wg.Done() }() } for i := 0; i < 5; i++ { wg.Add(1) go func() { mu.RLock() fmt.Println("(c) progress length =", len(ds)) mu.RUnlock() wg.Done() }() } wg.Wait() fmt.Println("(c) useRWMutex length =", len(ds)) } func main() { noLock() println("-----") useMutex() println("-----") useRWMutex() }
実行結果は以下の通りです。
排他制御を行っていない (a) では、同じ状態の ds
に対して複数の goroutine が ds = append(ds, Data{i})
を実行してしまうケースが発生するため、基本的に結果が 100 にはなりません。
実行結果
> go build go_mutex_sample.go > go_mutex_sample (a) noLock length = 95 ----- (b) useMutex length = 100 ----- (c) progress length = 71 (c) progress length = 72 (c) progress length = 72 (c) progress length = 72 (c) progress length = 72 (c) useRWMutex length = 100
Rust の場合
次は Rust による実装です。
ここでは thread::spawn
で並行処理を実施し join
で待機しています。
基本的に Rust では、Go 言語で実装した noLock
のようなスレッドセーフでは無い処理はコンパイルエラーとなって実装できないように工夫されています。
スレッド間で安全に所有権を共有するには、スレッドセーフな参照カウントのポインタである Arc
を使用する事になります。
排他制御なしの (a) の処理(下記コードの no_lock
)をコンパイルが通るように一応は実装してみましたが、この Arc::get_mut(&mut ds)
は常に None
を返すので ds.push(Data(i))
の処理は実行されません。
rust_mutex_sample.rs
use std::thread; use std::sync::{Arc, Mutex, RwLock}; struct Data(i32); // (a) fn no_lock() { let mut hs = Vec::new(); let ds = Arc::new(Vec::new()); for i in 0..100 { let mut ds = ds.clone(); hs.push( thread::spawn(move || { if let Some(ds) = Arc::get_mut(&mut ds) { // 上記は常に None となるので下記処理は実行されない ds.push(Data(i)) } }) ); } for h in hs { let _ = h.join(); } println!("(a) no_lock length = {}", ds.len()); } // (b) fn use_mutex() { let mut hs = Vec::new(); let ds = Arc::new(Mutex::new(Vec::new())); for i in 0..100 { let ds = ds.clone(); hs.push( thread::spawn(move || { if let Ok(mut ds) = ds.lock() { ds.push(Data(i)); } }) ); } for h in hs { let _ = h.join(); } println!("(b) use_mutex length = {}", ds.lock().unwrap().len()); } // (c) fn use_rwlock() { let mut hs = Vec::new(); let ds = Arc::new(RwLock::new(Vec::new())); for i in 0..100 { let ds = ds.clone(); hs.push( thread::spawn(move || { if let Ok(mut ds) = ds.write() { ds.push(Data(i)); } }) ); } for _ in 0..5 { let ds = ds.clone(); hs.push( thread::spawn(move || { if let Ok(ds) = ds.read() { println!("(c) progress length = {}", ds.len()); } }) ); } for h in hs { let _ = h.join(); } println!("(c) use_rwlock length = {}", ds.read().unwrap().len()); } fn main() { no_lock(); println!("-----"); use_mutex(); println!("-----"); use_rwlock(); }
実行結果は以下の通りです。
no_lock
内の ds.push(Data(i))
は実行されないので結果は 0 となります。
実行結果
> rustc rust_mutex_sample.rs > rust_mutex_sample (a) no_lock length = 0 ----- (b) use_mutex length = 100 ----- (c) progress length = 99 (c) progress length = 99 (c) progress length = 99 (c) progress length = 99 (c) progress length = 99 (c) use_rwlock length = 100