Skip to content

Latest commit

 

History

History
149 lines (115 loc) · 6.55 KB

File metadata and controls

149 lines (115 loc) · 6.55 KB

Cancellation

Dropping a Future before it resolves to a value is called cancelling the Future. This is a very useful and powerful idea, but it’s subtle and contains some gotchas.

Normally, when you do something that produces a Future, you will poll that Future until it resolves — usually with await. However, this isn’t the only thing you can do; you have two more options:

  1. Don’t poll it, leaving it sitting around, or

  2. drop it.

This doc is concerned with the second one.

1. Times when cancellation happens

Or, "why would my callers drop me?"

Cancellation can happen if you call an async fn without using the result, as in

async fn do_stuff() { ... }

do_stuff(); // note that we did not await

But that’s kind of a silly example, because you probably wouldn’t do that in practice — particularly since it will get you a compiler warning!

Most realistic examples of cancelling a Future involve situations where you could have polled it, and in fact may have polled it a few times already, but then circumstances changed and you decided to do something else. This happens when, for example, using select to implement timeouts:

futures::select! {
    data = queue.pop() => process(data);
    _ = sleep_for(TIMEOUT) => {
        // No data arrived in time! Let's move on and do something else.
    };
}

This uses the select macro from the futures crate to await on two futures: one that is attempting to pop data from a queue, and one that resolves automatically after a delay. select will poll all the given futures until one resolves, and then drop them all. In other words, all the futures that didn’t resolve get cancelled.

Tip
Cancellation relies on the fact that dropping stuff in Rust happens synchronously, i.e. by the time execution gets to the next statement, the drop is complete. Other languages that make heavy use of futures or promises, such as JavaScript and Java, typically can’t guarantee that, which makes cancellation much less useful.

2. Cancel-correctness and cancel points

We’ll refer to the property of code that doesn’t break when its futures get cancelled as "cancel-correctness," by analogy to "const-correctness."

To be cancel-correct, a future should behave reasonably if it’s dropped any time before it resolves — before it’s polled, or after it’s been polled some but hasn’t resolved.

Fortunately, a future can’t be dropped while it’s being polled. This is a key difference between cancellation, which always happens while a future is "dormant," and similar mechanisms in other languages such as thread interruption or signals — those other mechanisms can happen at any time, even in the middle of straight-line code. Points in the code where cancellation is possible are called cancel points, and you can spot them when reading the code.

In an async fn, every await is a potential cancel point. Any macro that expands to contain an await is also a cancel point; these might be macros you write yourself, or common examples like futures::select!.

Other than macros, you don’t have to read any other code to identify cancel points in a function. In particular, calling a function — any function — can’t create a cancel point in the caller’s code. Only await can do that.

Note
This is a key difference between cancel-correctness in Rust and exception-correctness in languages like C++, where any function could potentially throw, and you may need to read and understand the other functions' APIs to identify throw-points. (I am ignoring panic! because lilos primarily targets systems that don’t unwind on panic!.)
Tip
If you’re writing a Future by hand, the poll function won’t contain any cancel points — the Future can only be dropped after poll returns.

2.1. Writing cancel-correct code

The short version is to assume that every await might be your last — but how?

First, if your code has invariants to maintain, make sure they hold at any cancel point. It’s okay to temporarily break invariants between cancel points, because nobody will be able to see it. (This is a nice thing about cancel points being explicit and visible.)

Second, if your code is managing a resource, think about what your callers would expect might happen to that resource if they cancel you. For instance, if you’ve allocated memory, you probably want to free it; if you’ve locked a mutex, it would be very polite to unlock it.

You might have noticed something in common between the two examples above — memory and mutexes — which is that both normally use ownership-based resource management in Rust (aka RAII). In cases like this, you will most likely get cancel-correctness for free, because the drop impls of any local variables in your async fn (or any fields in your Future type) will get called when it’s dropped.

This includes the other futures that you await inside yours. If your future composes a bunch of other futures, as long as they are all cancel-correct, your future is probably cancel-correct by default.

The main exception to this rule is when you’re updating a data structure that outlives your future, and await-ing during the process. For instance, if you set a "being updated" flag at the start, you will want to ensure that flag gets cleared on cancellation. This means you need to add custom code at drop. There are three ways to do this in practice:

  1. Instead of an async fn you can write a Future type by hand and give it a Drop impl. This is a huge change, and is probably your last resort.

  2. You can write a custom resource type used inside your future, whose only role is to have a custom Drop impl. This is nearly as flexible as a custom Future type, but lets you continue using async fn for most of your code.

  3. You can use the scopeguard crate (not the scope_guard crate, which I haven’t tested but sure looks like typo-squatting), which is basically a generalized version of the previous option.

3. Cancellation in the lilos API

All the futures in the core lilos API, and in the optional utility packages like mutex and spsc, have been written to have well-defined cancellation behavior. Generally they’re designed to do "what you’d expect;" for specifics, look for the Cancellation sub-headings in the docs for each function.

This means that, in most cases, if you write an async fn using the lilos async APIs, you’ll be cancel-correct by default.