diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2024-04-10 00:33:30 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2024-04-10 00:33:30 -0400 |
| commit | 9c27d3906ad171ea500af7e89b3d41058684b7ab (patch) | |
| tree | 5ea15b2d654c2a98ed9ee96bf588f2467cfeabf3 | |
| parent | fb8a044730425759917b7286ed503a8a6cad760f (diff) | |
Rust refs to futures
| -rw-r--r-- | content/code/rust-future-refs-are-futures.md | 52 |
1 files changed, 52 insertions, 0 deletions
diff --git a/content/code/rust-future-refs-are-futures.md b/content/code/rust-future-refs-are-futures.md new file mode 100644 index 0000000..c9b6913 --- /dev/null +++ b/content/code/rust-future-refs-are-futures.md @@ -0,0 +1,52 @@ +--- +title: A reference to a Future is a Future (sort of) +date: 2024-04-10T00:04:37-04:00 +--- +If `T` implements `Future`, then `&mut T` also implements `Future`. You can use this fact to add cancellation safety to a non-cancel-safe future. +<!--more--> +All of this comes down to [`select!`](https://docs.rs/tokio/latest/tokio/macro.select.html) as a tool for running concurrent jobs. From its documentation: + +> When using select! in a loop to receive messages from multiple sources, you should make sure that the receive call is cancellation safe to avoid losing messages. +> +> […] +> +> To determine whether your own methods are cancellation safe, look for the location of uses of `.await`. This is because when an asynchronous method is cancelled, that always happens at an `.await`. If your function behaves correctly even if it is restarted while waiting at an `.await`, then it is cancellation safe. + +The docs go into more depth about why this is, but the bottom line is that, when `select!`ing against _N_ futures, _N_ - 1 of them will be cancelled (and dropped), abandoning any work they were doing up to the last await point. In practice, it's _hard_ to make a future that is entirely cancellation safe: it has to do its work atomically, or in ways that can be restarted or unwound after the fact. Cancellation safety, unfortunately, does not compose - a sequence of two cancellation-safe operations is not necessarily a cancellation-safe operation. + +What I didn't know when I last used this, and what the docs unfortunately do not say, is that stored futures can be made cancellation-safe by taking references to them. A reference to a future is in most ways a transparent The operations they perform will update the stored future, but only the reference, and not the underlying stored future, will be cancelled. + +Thus: + +```rust +async fn run_tasks() { + loop { + tokio::select! { + a = task_a() => { completed(a) }, + b = task_b() => { completed(b) }, + } + } +} +``` + +requires that both `task_a` and `task_b` return cancellation-safe futures, as the loop will start both tasks from the beginning each time it enters the `select!`. On the other hand: + +```rust +async fn run_tasks() { + let fut_a = task_a(); // no .await + let fut_b = task_b(); // ditto + tokio::pin!(fut_a); + tokio::pin!(fut_b); + + loop { + tokio::select! { + a = &mut fut_a => { completed(a) }, + b = &mut fut_b => { completed(b) }, + } + } +} +``` + +does not require that either `task_a` or `task_b` be cancellation-safe, as the loop will resume both future at their respective last await points each time it enters the `select!`. + +Note that both futures have to remain awaitable, though - if either of them terminates, this will panic on the next `select!`. This makes this technique more useful with [`futures::select_all`](https://docs.rs/futures/0.3.30/futures/future/fn.select_all.html), where a completed task can be taken out of the argument list for future selects. |
