概要
- futurelock は、非同期Rustで発生する特有のデッドロック現象
- 一つのタスクが複数Futureを管理し、その一部がリソースを保持したまま他のFutureの進行を妨げる構造
- tokio::select! や Mutex の使い方次第で簡単に発生
- 問題の再現例と発生メカニズムの詳細な解説
- 回避策 や設計上の注意点も紹介
futurelock(フューチャーロック)の概要と問題点
- futurelock とは、Future Aが所有するリソースをFuture Bが必要とするが、両方を管理するTaskがAをもうpollしないため発生するデッドロック現象
- Rustの非同期設計において 非常に見落としやすい罠
- tokio::select! や tokio::sync::Mutex などの組み合わせで頻発
再現コード例
- 複数タスクで共有する Mutex を用意
- バックグラウンドタスクでMutexを5秒間保持
- メインタスクで tokio::select! により
future1: Mutex獲得を試みるFuturefuture2: 500msスリープするFuture
- 500ms経過後、
future2がReadyとなりselect!は第2分岐へ進む - しかし
future1はまだMutex待ち状態で、今後pollされずリソースが解放されない
問題の本質
- tokio::select! は、最初にReadyとなったFutureの分岐だけを実行し、他のFutureはdropされる
- ただし、
&mut future1の参照がdropされても、future1本体はdropされず未完了のまま
- ただし、
- Mutexは フェアな順番 で待機者にロックを譲る
- 先に待機した
future1が優先されるが、pollされなくなったため進行不能
- 先に待機した
- 同じタスク が複数のFutureを管理し、一部しかpollされなくなる設計上の落とし穴
FAQ よくある疑問
- なぜMutexが他のFutureを起こさないのか?
- Mutexは次の待機者(future1)を正しく起こしているが、タスクがそのfutureをpollしないため意味がない
- tokio::select!は複数Futureをpollし続けるのでは?
- 最初にReadyになった分岐だけにコミットし、他のFutureはpollされなくなる仕様
- future1はキャンセルされないのか?
&mut future1の参照がdropされるだけで、future1本体は生き続けるため、リソース解放が起きない
futurelock発生パターンと設計上の注意点
- タスクT がFuture F1の完了待ちでブロック
- F1 がリソース獲得などでF2に依存
- F2 がTによるpollを待つが、TはF1しかpollしない
- tokio::select!で
&mut futureを分岐に渡し、他分岐でawaitすると高確率で発生 - FuturesUnordered や独自Future実装でも同様のパターンに注意
具体的な回避策
- select!に 所有権付き(owned)Future を渡すことで、分岐移行時に確実にdropされリソース解放
- select!後に不要なFutureを 明示的にdrop する
- タスク分割 (tokio::spawn等)で各Futureを独立したタスクとして管理
- リソース取得順序や設計の見直し
タスクとFutureの違い
- タスク はランタイムが実行する最上位の単位
- Future はタスク内でpollされる実行単位
- select!やFuturesUnorderedで 単一タスク内で複数Futureの同時進行 は可能だが、parallelism(並列性)はない
- 並列実行が必要な場合は 各Futureをspawn して独立タスク化
まとめと参考情報
- futurelock はプログラム上正しく見えても発生しうる深刻な問題
- Rust非同期設計における リソース管理とタスク分割の重要性
- 詳細な議論・実例は以下を参照