###### tags: `Rust` `lazy_static` `Mutex` `RwLock` `sync` # Mutex, RwLock でスレッド間リソース共有(グローバル変数も) # リソースについて 別のスレッド(関数)があるリソースを参照しているときに、 リソースを変更しようとすると、値に整合性が取れなくなる場合がある。 そこで、 - 誰かがwriteしているとき、他のスレッドにread/writeさせない - 誰かがreadしているとき、他のスレッドにwriteさせない というリソースロックの仕組みが Mutex と RwLock ## 別名 他のプログラミング言語ではSemaphoreというのがある RwLockはSemaphoreと似てる? # 比較 | | Mutex | RwLock | | -------- | -------- | -------- | | 説明 | リソースの利用者は、 読み書き関わらず1人だけ | 書き込みは1人 読み込みだけ複数人に許す (Rustの借用ルールに似ている) | | & | 1 | multi | | &mut | 1 | 1 | | lockメソッドの種類 | .lock() | .read() / .write() | # Mutex ## new(T) ## mux.lock() - リソースがロックされていなければ、取得 - 読み書き可能なMutexGuardオブジェクト(リソースへのポインタ)が帰ってくる。 - そのままで受け取ればread only - mut で受け取れば read write - リソースがロックされていれば、取得できるまでスレッドをブロッキング - スレッドをブロックしない mux.try_lock() もある ```rust= let mux = std::sync::Mutex::new(100); { let mut writer = mux.lock().unwrap(); *writer = 200; // Deref して書き込み } { let reader = mux.lock().unwrap(); println!("{}", reader); } ``` ## .get_mut() & .into_inner() どちらも、信頼できない参照先を返す。 - into_inner() : ポインタの中身を返す - get_mut() : &mut の参照を返す。 これらを mut で移動すると、実質的にMutexの管理で値を守らないので スレッドセーフの利点がなくなる。 ## is_poisoned() : bool lockを保持したスレッドがpanic!したかどうか ### 絶対やってはいけないアンチパターン 同じスコープ内で lock を解除しないまま新しいロックを取得しようとすると 簡単にデッドロックできる。 ```rust= let mux = std::sync::Mutex::new(100); let mut writer = mux.lock().unwrap(); *writer = 200; // Deref して書き込み let reader = mux.lock().unwrap(); // リソースがunlockされないので無限にループ println!("{}", reader); ``` # RwLock Mutexの違いは、lock() / try_lock() メソッドが - read() / try_read() - write() / try_write() の2つに増えたくらいしかない。 ```rust= let rwlock = std::sync::RwLock::new(100); { let mut writer = rwlock.write().unwrap(); *writer = 200; // Deref して書き込み } { // 読み込みだけはいくらでもOK let r1 = rwlock.read().unwrap(); let r2 = rwlock.read().unwrap(); let r3 = rwlock.read().unwrap(); let r4 = rwlock.read().unwrap(); let r5 = rwlock.read().unwrap(); println!("{}, {}, {}, {}, {}", r1, r2, r3, r4, r5); } ``` # Mutex/RwLock を局所的に使うテクニック テスト関数などで、グローバルなstaticリソースの書き込みをテストする場合など、 - テストの初期化 - 関数の実行 でそれぞれstaticリソースを操作する。 この際に、lockを取得したままだと関数の実行がうまく行かない場合がある。 mutexのlockは、`{}` スコープを抜ける際に自動で Dropトレイトの実装を 実行してlockを解除する。 **{}により、局所的にリソースロックを掛ける** # Mutex と RwLock どちらを使うか。 個人的には、 - 読み書きどちらをするのか明示的でわかりやすい - Readが複数許される ~~という点で、 **RwLockはMutexの上位互換** だと思う。~~ Simple_Redis の client が、RwLockに対応していないっぽい。 Mutexだとエラーにならない。 ~~Mutexにしか対応していないようなリソースの型を使うときはMutexを使う。~~ そもそも、simple_redisのクライアントにはSyncトレイトが実装されていないので ArcもMutexもRwLockも使えない。 # static 変数との親和性 Rustにおいて、 static リソースについては - 読み込みはできる - 書き込みはunsafeを使わないとできない - staticがグローバルな変数故に、複数のスレッドで共有されるため ということで、 `unsage{}` ブロックを使わないと書き込みできない static変数だが、 Mutex/RwLock がlockメソッドで返す **Guardオブジェクト**は**内部ミュータビリティ** だと思われるので、 unsafeを使わずに書き込みできる。 データベースへの接続を static変数として、グローバルに使い回す例 ```rust= lazy_static! { pub static ref CONNECTION: RwLock<redis::Connection> = { let c = CLIENT.get_connection().unwrap(); RwLock::new(c) }; } ``` ```rust= fn get_by_name(unique_name: &str) -> Result<User, String> { // redisのクエリを実行するには、 &mut con つまり書き込み可能な接続が必要 let mut con = CONNECTION.write().unwrap(); let result: Result<String, redis::RedisError> = con.hget(userDict, unique_name); // let result: Result<String, redis::RedisError> = redis::cmd("hget").arg(userDict).arg(unique_name).query(con); // こちらの形式でもかける match result { Ok(json) => Ok(serde_json::from_str(&json).unwrap()), Err(_) => Err(String::from("failed to get user")), } } ``` redis クレートのクエリ実行は、Connectionオブジェクトにコマンドを書き込んで実行しなければならない。 よって、RwLock の write() ロックで &mut な参照を持ってくる。 しかし、static変数の CONNECTIONオブジェクトは mut ではない。 RwLockでラップすることで安全に使えるようになった。