Three problems of pinning

When we developed the Pin API, our vision was that “ordinary users” - that is, users using the “high-level” registers of Rust, would never have to interact with it. We intended that only users implementing Futures by hand, in the “low-level” register, would have to deal with that additional complexity. And the benefit that would accrue to all users is that futures, being immovable while polling, could store self-references in their state.

Things haven’t gone perfectly according to plan. The benefits of Pin have certainly been accrued - everyone is writing self-referential async functions all the time, and low-level concurrency primitives in all the major runtimes take advantage of Pin to implement intrusive linked lists internally. But Pin still sometimes rears its ugly head into “high-level” code, and users are unsurprisingly frustrated and confused when that happens.

In my experience, there a three main ways that this happens. Two of them can be solved by better affordances for AsyncIterator (a part of why I have been pushing stabilizing this so hard!). The third is ultimately because of a mistake that we made when we designed Pin, and without a breaking change there’s nothing we could about it. They are:

  1. Selecting a Future in a loop.
  2. Calling Stream::next.
  3. Awaiting a Future behind a pointer (e.g. a boxed future).

Selecting in a loop

As I wrote in my post about poll_next, one problematic pattern today is selecting in a loop:

loop {
    select! {
        result = async_function1() => // ...
        result = async_function2() => // ...
    }
}

The problem is that what this does is construct futures for each branch, awaits one of them finishing, and then cancels the unfinished branches as the iteration ends. In the next iteration, all of the futures are constructed again, and then canceled again. If canceling a future is meaningful behavior, this is probably not what a user wants to do.

The way to implement this correctly right now is to hoist the async functions out of the loop, pin them, and fuse them. As in:

let future1 = pin!(async_function1().fuse());
let future2 = pin!(async_function2().fuse());

loop {
    select! {
        result = &mut future1 => // ...
        result = &mut future2 => // ...
    }
}

The reason they need to be pinned is that you are polling them in the loop by reference; if they aren’t pinned, you could poll them in the loop, then move them and start polling them somewhere else. This would invalidate any self-references stored in their state.

The solution to this problem is to introduce a new API, based on AsyncIterators, which can replace select in a loop. This would be a merge! macro, which takes a set of AsyncIterators and polls all of them, yielding when each of them returns an item. This would eliminate the need to hoist futures out of the loop, because the futures would not be canceled as the merge! macro iterates. For example:

merge! {
    result = once(async_function1()) => // ...
    result = once(async_function2()) => // ...
}

Stream::next

Another way that users sometimes find they need to pin values in place is when they are calling the next method on a Stream which doesn’t implement Unpin.

The next method has a requirement that Self: Unpin; this is because next takes the self type by mutable reference, but the underlying poll_next method takes the self type by pinned reference. It’s only possible to turn a pinned reference into a mutable reference safely if the type implements Unpin.

If the Stream doesn’t implement Unpin, the solution is to pin it: a pinned reference to a stream implements both Unpin and Stream, and so next can be called on it. This works, but it forces the user to encounter pinning in the high-level register.

The Rust project intends to ship AsyncIterator which will replace Stream. Avoiding this problem would be desirable. One way would be to make AsyncIterator not take a pinned reference (this could be done by changing the signature of poll_next or by using an async method), but this would eliminate the beneficial affordances that pinning the AsyncIterator provides. My previous posts have gone into some detail about why that would be the wrong choice; another reason is that it would mean that async generators could not be self-referential.

Instead, the best solution is to make it so that users don’t have to call the next method so much. Right now, the way that users iterate over a stream is this:

let stream = pin!(stream);
while let Some(element) = stream.next().await {
    // ...
}

Instead, the solution for iterating over an AsyncIterator should be a first class syntax which handles pinning the stream in place as a part of its desugaring. This, which is usually called a for await loop, would look like this:

for await element in async_iter {
    // ...
}

I think there are only two cases in which users would still need to call next if this syntax existed:

  1. When they only want to take some fixed number of elements from the iterator, and handle each of them differently (usually, they only want to take one element). Special combinators, let’s say a first combinator, that take the AsyncIterator by value and return the first element, or first N elements, would be an option. Because they would be by-value, they could pin the AsyncIterator for you.
  2. When they want to consume items from the iterator and then move it afterward. This just requires that the iterator be pinned or implement Unpin, because its exactly what requires pinning to be memory safe!

I find either of these cases to be exceedingly rare, but the second one especially so. By implementing for await and possibly a first combinator, the problem of pinning to iterate over an AsyncIterator can be resolved except in that rare case that the user actually wants to do something that isn’t safe without pinning.

Awaiting an indirect future

The last big way that pinning shows up is when awaiting a future that you have access to indirectly, behind a pointer of some kind. There are two main ways this shows up:

  1. If you want to await a future trait object, that trait object needs to be behind a pointer because it isn’t Sized.
  2. If you want to write a recursive async function, that recursive call needs to be behind a pointer so that state of this future isn’t infinitely large.

The thing that’s really frustrating about this is that for Box, it’s really a mistake. I remember a conversation that Taylor Cramer and Ralf Jung and I had in 2018 in which we made a key decision about the Pin interface, and I don’t think any of us understood that it would have this implication. If we had, I think we would have gone the other way. Let me elaborate.

You see, we decided that Box<T> always implements Unpin, even if the type owned by the box doesn’t implement Unpin. This was actually an arbitrary decision: although it made sense to us that Box<T> would be Unpin, it wasn’t necessary. For mutable references it is necessary, but for box we just felt that, since the Box doesn’t itself contain any self-references, it makes sense to say that it is also Unpin, plus it’s consistent with the implementation of Unpin for non-owning reference types.

The result of this fact is that Box<T: Future> doesn’t implement Future. If it did, you could poll the future via the Box implementation, but then move the future out of the Box because Box implements Unpin. This would be a soundness bug. Instead, only Pin<Box<T: Future>> implements Future.

If we had omitted that implementation of Unpin for Box, we could have instead had Box<T: Future> implement Future. This would have made it possible to await a boxed future without pinning it. You would still need to pin borrowed futures (i.e. futures behind a mutable reference) to await them, but that’s less common and it’s just fundamentally required for soundness.

I actually think this was the biggest mistake we made in designing async/await, and I’m pretty sure I’m the only person who really knows about it or ever thinks about it. I don’t think removing that impl would be possible now, even across an edition boundary, so we may be stuck with that mistake forever.

Conclusion

Circling back, I want to highlight that with the exception of boxed futures, better support for AsyncIterator would solve almost all of the examples of pinning in high-level code that I experience today:

  1. A merge! macro would avoid the whole problem of hoisting futures out of the loop, and so users wouldn’t encounter pinning there.
  2. A for await loop would eliminate the need to call the next adapter to iterate through an AsyncIterator, avoiding the requirement to pin the AsyncIterator before calling next.
  3. A first combinator could eliminate most of the remaining need to use the next adapter, leaving only the situations in which you actually need to pin for safety.

This is a part of my motivation for pushing AsyncIterator so hard. Especially for await, which is a language construct, can’t just exist in the ecosystem. First class support for this async iteration will resolve major pain points in the async ecosystem.