[Rust] if let と mutex lock の組み合わせでハマった

## （前段）複数スレッドから１つの変数を編集する

Rustでマルチスレッドのプログラムを書く場合
`Arc`と`Mutex`を組み合わせることで、1つの変数を複数のスレッドから参照・変更することができる

```rust
// 複数のスレッドでカウントアップできるカウンター変数
// clone してからスレッドにmoveして持ち込む
let counter = Arc::new(Mutex::new(0));

// スレッドの中では、ロックしてから使う
let mut num = counter.lock().unwrap();
*num += 1;
```

https://doc.rust-jp.rs/book-ja/ch16-03-shared-state.html

> リスト16-15: Arc<T>を使用してMutex<T>をラップし、所有権を複数のスレッド間で共有できるようにする

### 本題の前に、`Arc`って何？

`Rc<T>`: reference counting(参照カウント)の省略形
参照の数が0になったら、その値は片づけられる

`Arc<T>`は`Rc<T>`の頭に`a`がついたもの
`a`は`atomic`の意味らしい
データベースの話でACID特性というものがありますが、その`A`とニュアンスが近いのかもしれない

https://doc.rust-jp.rs/book-ja/ch15-04-rc.html

次から本題

## この記事は

最近の出来事で
マルチスレッドのプログラムにおいて、`Mutex`のロックをうまく使いこなせず
１つのスレッドの待ち状態が起き、原因がよくわからなかった

いくつかの検証コードを書いて、それぞれの挙動を観察する

## 結論

`if let Some(T) =`の右辺にMutexのロックを置くと
後続の`else`文でもロックは継続している

しかもドロップのタイミングは不定

`if let Some`の右辺でロックは書かない（パターン１）
なるべく`if`の外でロックを書く（パターン３）
そしてロックは不要となった所で明示的にドロップする（パターン３）

### ドロップタイミングが不定：メソッドチェーンの中の`lock`

mut参照の前に書かれたロックは、どこでドロップするかわからないので
別途`let`で束縛するようコンパイラから指摘される

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/c25d9858-824b-be4c-1f54-91a8313231b9.png)

## 2023年10月27日_追記

@gorilla0513さんからコメント&深掘り検証をしていただきました

https://qiita.com/t13801206/items/18e4bd372ee258e26d1a#comment-1ad0733bbd2866ab3c74

`if let ... else`は`match`のシンタックスシュガーであり
デシュガーすると、ロックは`match`を抜けるまで（`else`を抜けるまで）つづく
ということがわかりました🙌

詳細は下記記事にて。

https://zenn.dev/skanehira/articles/2023-10-26-rust-if-let-mutex

## 基本のコード

- スレッドを2つ生成する
- 各スレッドで、`println!`を1,000回出力する
- `println!`出力のあと、４パターンの検証コードにしたがって、スレッドをスリープさせる

```rust
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::{thread, time};

fn main() {
    let counter: Arc<Mutex<i32>> = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for i in 0..2 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            for j in 0..1000 {
                println!("thread {}, loop {}", i, j);
                // ---- ここから
                // ★４パターンのお話をします★★★
                // ---- ここまで
            }
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }
}
```

## パターン1：`if let Some`

この記事を書くきっかけになったコードがこちら

カウンターをロックして、`Some()`で結果を取り出して
if文の中で処理をおこなう

ただし、ロックした中身が`None`であれば、一定時間待つ

:::note info
説明を簡単にするために、わざと無意味なコードを書いてます
実際はARPテーブルのように、所望のIPアドレスがARPテーブルに有るか無いかによって
処理を分けるコードを書いてました
:::

```rust:パターン１のコード
if let Some(_) = counter.lock().unwrap().checked_div(i) {
    // i = 1 のときは何もしない
} else {
    // i = 0 のときは、100ms待つ
    thread::sleep(time::Duration::from_millis(100));
}
```

期待していた結果は**スレッド0が待ち状態にある間に、スレッド1が1,000回ループを終える**ことだった

しかし、結果はスレッドは0と1を交互に実行している様子だった
スレッド1が連続してループを消化する期間もあるが
0,1が交互に現れて、息継ぎするようにストップアンドゴーで画面が更新される

```text:パターン１の結果
thread 1, loop 771
thread 1, loop 772
thread 1, loop 773
thread 0, loop 21
thread 1, loop 774
thread 0, loop 22
thread 1, loop 775
thread 0, loop 23
thread 1, loop 776
thread 0, loop 24
thread 1, loop 777
```

1,000回ループするのに100msは短し時間ではない
つまりスレッド0がロックを占有して、スレッド1が待ち状態になっていることを意味する

スレッド0は`else`に進んで100ms待つが、待っている間も`if let`で`counter`のロックを
持ち続けていると予想される

## パターン2：`if`

`if let`を`if`に書き換えた

```rust:パターン２のコード
if counter.lock().unwrap().checked_div(i).is_some() {
    // i = 1 のときは何もしない
} else {
    // i = 0 のときは、100ms待つ
    thread::sleep(time::Duration::from_millis(100));
}
```

この場合は、スレッド1が1,000回ループを一瞬で消化する

```text:パターン２の結果
thread 1, loop 992
thread 1, loop 993
thread 1, loop 994
thread 1, loop 995
thread 1, loop 996
thread 1, loop 997
thread 1, loop 998
thread 1, loop 999
thread 0, loop 2
thread 0, loop 3
thread 0, loop 4
thread 0, loop 5
thread 0, loop 6
```

スレッド1はスレッド0が100msスリープしている間に何かロックを待つことはない
`if`の右辺を評価し終えたあと、スレッド0はカウンターとは無縁になる

## パターン3：`if let Some` + `drop`

パターン１と同様に`if let Some`を使う
ただし、`lock`変数は`else`に入ったところで`drop`する

```rust:パターン３のコード
let lock = counter.lock().unwrap();
if let Some(_) = lock.checked_div(i) {
    // i = 1 のときは何もしない
} else {
    // i = 0 のときは、ロックをドロップしてから、100ms待つ
    drop(lock);
    thread::sleep(time::Duration::from_millis(100));
}
```

lockはelseに入ったところでドロップされるので
スレッド0が100ms待っている間に、スレッド1は1,000回ループを終えられる

```text:パターン３の結果
thread 1, loop 995
thread 1, loop 996
thread 1, loop 997
thread 1, loop 998
thread 1, loop 999
thread 0, loop 2
thread 0, loop 3
thread 0, loop 4
thread 0, loop 5
thread 0, loop 6
thread 0, loop 7
thread 0, loop 8
thread 0, loop 9
```

## パターン４：`if let Some` + `drop`しない

これはパターン１と同じか・・・

`lock`のスコープが`else`のあとまでつづくのが明確なので
スレッド0のスリープが終わるまでロックが継続するのがわかりやすい

```rust:パターン４のコード
let lock = counter.lock().unwrap();
if let Some(_) = lock.checked_div(i) {
    // i = 1 のときは何もしない
} else {
    // i = 0 のときは、ロックをドロップしてから、100ms待つ
    // drop(lock); <<<--- コメントアウト
    thread::sleep(time::Duration::from_millis(100));
}
```

スレッド1はスレッド0のスリープに影響を受けている様子がわかる
処理を何もしないスレッド1のループはなかなか終わらない

```text:パターン４の結果
hread 0, loop 13
thread 1, loop 231
thread 0, loop 14
thread 1, loop 232
thread 0, loop 15
thread 1, loop 233
thread 0, loop 16
thread 1, loop 234
thread 0, loop 17
thread 1, loop 235
```

パターン１よりもスレッド1のループの周回速度が遅くなっているのは
スコープが広くなっているからか・・・？

## おわりに

よくできたライフタイムの仕組みに乗っかっているのに
明示的に`drop`する必要が生じるってことは
なにか根本的に間違っている気がしている

### 余談1：DiscordがGoからRustに切り替えた

ガベージコレクションがないRustの良さが書かれた記事

https://atmarkit.itmedia.co.jp/ait/articles/2002/10/news038.html

### 余談2：RwLockっていうものがあってな

ARPテーブルのような、だいたいは読み取りだけで、たまにテーブル更新（書き込み）するなら
`Mutex`ではなく、`std::sync::RwLock`でもいいのかなー、と
最近よんだ記事で知った

https://laysakura.github.io/2022/03/30/rust-RwLock-writer-starvation/