2025/10/31 16:49 Futurelock: A subtle risk in async Rust

やっほー、ロボ子!今日も面白い記事を見つけたのじゃ!

こんにちは、博士。今日はどんな記事ですか?

今日はね、「プログラムは確実にデッドロックする」っていう、ちょっと怖い話なのじゃ。

デッドロックですか!それは大変ですね。詳しく教えてください。

`do_stuff()`の中の`tokio::select!`が原因らしいのじゃ。`future1`がロックを取得しようとしてブロックして、`future2`は0.5秒待機するsleep futureらしい。

`tokio::select!`ですか。複数のfutureを同時にポーリングして、最初に完了したブランチに入るものですよね。

そうそう!でも、ここに落とし穴があるのじゃ。背景タスクがロックを5秒間保持している間に、`tokio::select!`が`future1`と`future2`をポーリングする。0.5秒後、`future2`がreadyになって、`tokio::select!`はそっちを選んでしまうのじゃ。

`future1`はどうなるんですか?

`future1`はドロップされないけど、`tokio::select!`はもう見てくれないのじゃ。そして、2番目のブランチで`do_async_thing("op2", ...)`が呼ばれて、新しいfuture `future3`がロックを取得しようとしてブロックする。

ロックの待機キューに`future1`と`future3`が並んで、同じタスクが両方のfutureを担当しているのに、片方しかポーリングしていない状態になるんですね。

その通り!4.5秒後、背景タスクがロックを解放して、`future1`にロックが渡される。でも、メインタスクは`future1`で起こされるけど、`tokio::select!`はもう別のブランチを選んでいて、`future3`しかポーリングしていない。`future3`は`future1`が所有するMutexでブロックされているから、永遠に止まってしまうのじゃ。

なるほど…。これがfuturelockというデッドロックの一種なんですね。

`tokio::sync::Mutex`はfairなMutexで、待機順にロックを渡すから、こういうことが起こりうるのじゃ。`tokio::select!`を使うときは、futureがドロップされる時の挙動をよく理解しておく必要があるぞ。

`tokio::select!`でポーリングしているfutureの1つが完了すると、他のfutureはドロップされるんですね。`&mut future1`はドロップされるが、`future1`自体はドロップされない、と。

そう!タスクはランタイムが実行するトップレベルのエンティティで、各タスクは1つのトップレベルのfutureを実行する。この仕組みを理解してないと、思わぬ落とし穴にはまることがあるのじゃ。

勉強になります。`tokio::select!`を使う際は、十分に注意が必要ですね。

そういうこと!最後に、ロボ子、デッドロックは一体何のロックが原因だと思う?

えっと…、Mutex、ですか?

ブー!残念!正解は…鍵が見つからないロック!…って、つまらないジョークでごめんのじゃ!
⚠️この記事は生成AIによるコンテンツを含み、ハルシネーションの可能性があります。