Futurelock: A subtle risk in async Rust
6 months ago
- #deadlock
- #rust
- #asynchronous-programming
- Futurelock is a type of deadlock in asynchronous Rust where a resource owned by one future is required by another, but the task responsible for both is no longer polling the first future.
- The issue was initially identified in oxidecomputer/omicron#9259 and can lead to subtle and hard-to-debug hangs.
- A common scenario involves using `tokio::select!` with `&mut future` and awaiting in one of the branches, leading to a deadlock if the futures share resources like a Mutex.
- Futurelock can also occur with `FuturesOrdered`/`FuturesUnordered` when awaiting another future after pulling from the stream.
- Debugging futurelock is challenging as it manifests as a hang, with tasks blocked on futures that are not being polled.
- Mitigation strategies include spawning futures in separate tasks, avoiding `&mut future` in `tokio::select!`, and not awaiting other futures in the body of a loop that pulls from a stream.
- Using bounded channels with `try_send()` instead of `send().await` can prevent channels from becoming part of a futurelock.
- Futurelock undermines Rust's goal of local reasoning about correctness, as the issue arises from the interaction of seemingly independent constructs.
- Potential solutions include writing Clippy lints to warn against risky patterns like using `&mut future` in `tokio::select!` or awaiting in a `select!` branch.