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:
- Selecting a
Future
in a loop. - Calling
Stream::next
. - 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 AsyncIterator
s, which can replace
select in a loop. This would be a merge!
macro, which takes a set of AsyncIterator
s 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:
- 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 theAsyncIterator
by value and return the first element, or first N elements, would be an option. Because they would be by-value, they could pin theAsyncIterator
for you. - 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:
- If you want to await a future trait object, that trait object needs to be behind a pointer
because it isn’t
Sized
. - 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:
- A
merge!
macro would avoid the whole problem of hoisting futures out of the loop, and so users wouldn’t encounter pinning there. - A
for await
loop would eliminate the need to call the next adapter to iterate through anAsyncIterator
, avoiding the requirement to pin theAsyncIterator
before calling next. - 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.