summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-04-10 00:33:30 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-04-10 00:33:30 -0400
commit9c27d3906ad171ea500af7e89b3d41058684b7ab (patch)
tree5ea15b2d654c2a98ed9ee96bf588f2467cfeabf3
parentfb8a044730425759917b7286ed503a8a6cad760f (diff)
Rust refs to futures
-rw-r--r--content/code/rust-future-refs-are-futures.md52
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.