Tracking Issue for #![feature(async_iterator)]
This is a tracking issue for the RFC "2996" (rust-lang/rfcs#2996).
The feature gate for the issue is #![feature(async_stream)].
About tracking issues
Tracking issues are used to record the overall progress of implementation. They are also used as hubs connecting to other relevant issues, e.g., bugs or open design questions. A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature. Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.
Steps
<!-- Include each step required to complete the feature. Typically this is a PR implementing a feature, followed by a PR that stabilises the feature. However for larger features an implementation could be broken up into multiple PRs. -->- [x] Implement the RFC (cc @rust-lang/XXX -- can anyone write up mentoring instructions?)
- [x] Adjust documentation (see instructions on rustc-dev-guide)
- [ ] Stabilization PR (see instructions on rustc-dev-guide)
Unresolved Questions
None currently
Implementation history
- <!--
Thank you for creating a tracking issue! 📜 Tracking issues are for tracking a
feature from implementation to stabilisation. Make sure to include the relevant
RFC for the feature if it has one. Otherwise provide a short summary of the
feature and link any relevant PRs or issues, and remove any sections that are
not relevant to the feature.
Remember to add team labels to the tracking issue.
For a language team feature, this would e.g., be `T-lang`.
Such a feature should also be labeled with e.g., `F-my_feature`.
This label is used to associate issues (e.g., bugs and design questions) to the feature.
-->
This is a tracking issue for the RFC "2996" (rust-lang/rfcs#2996). The feature gate for the issue is
#![feature(async_iterator)].About tracking issues
Tracking issues are used to record the overall progress of implementation. They are also used as hubs connecting to other relevant issues, e.g., bugs or open design questions. A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature. Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.
Steps
<!-- Include each step required to complete the feature. Typically this is a PR implementing a feature, followed by a PR that stabilises the feature. However for larger features an implementation could be broken up into multiple PRs. -->- [x] Implement the RFC (cc @rust-lang/XXX -- can anyone write up mentoring instructions?)
- [x] Adjust documentation (see instructions on rustc-dev-guide)
- [ ] Stabilization PR (see instructions on rustc-dev-guide)
Unresolved Questions
- [ ] Clarify the panic behavior of
StreamandIteratorhttps://github.com/rust-lang/rust/pull/79023#discussion_r538975223- [ ] Add a panic section to
Iterator, clarifying panic behavior. The panic behavior betweenStreamandIteratorshould be consistent.
- [ ] Add a panic section to
- [ ] Restore
Stream::next. This was removed from the RFC because it prevents dynamic dispatch, and subsequently removed from the implementation. This should be resolved before stabilizing.- [ ] Investigate whether we can move the trait from
fn poll_nexttoasync fn nextonce we can useasyncin traits.
- [ ] Investigate whether we can move the trait from
- [ ] Investigate as part of keyword-generics whether we can merge
IteratorandAsyncIteratorinto a single trait which is generic over "asyncness". - [x] Should we name this API
AsyncIteratorinstead? https://github.com/rust-lang/rust/issues/79024#issuecomment-816868752
Implementation history
Yosh at 2020-12-18 16:12:44
Is there a plan to include a
core::stream::StreamExt(similar to the one in the futures crate) as well?Kai Mast at 2021-02-08 21:50:09
Is there a plan to include a
core::stream::StreamExt(similar to the one in the futures crate) as well?The plan is to add methods directly onto
Streammuch like methods exist onIterator, but we want to do so in a way that won't cause ambiguities with ecosystem-defined methods in order to not accidentally break existing codebases when upgrading to newer Rust versions.Yosh at 2021-02-09 12:51:42
Some discussion on Zulip raised the question of naming. With full acknowledgement to the fact that the name
Streamhas a long history in the async ecosystem, multiple people observed that something likeAsyncIteratoror similar might be much more evocative for new users. Such a name would allow people to map their existing understanding of iterators. "I understandIterator, and I understand async, and this is an async version ofIterator".Josh Triplett at 2021-04-09 18:16:51
As a new, inexperienced user, I find
AsyncIteratorway more foreign thanStream, to be honest... perhaps I'm not too involved in compiler discussions and I don't see how the jargon clicks together though :-S ... also, blogposts are already being written aboutStream, so changing the name mid-flight will only breed confusion, I reckon?Roman Valls Guimera at 2021-04-10 04:41:55
Prior art on "iterator" and "async iterator" naming schemes in other languages:
- JavaScript:
Symbol.Iterator,Symbol.AsyncIterator - C#:
IEnumerable,IAsyncEnumerable - Python:
__iter__,__aiter__ - Swift:
Sequence,AsyncSequence
Yosh at 2021-04-15 12:34:24
- JavaScript:
Context: We're prototyping some simple CLI app functionality. We're following the mini-redis example code where we can.
We've bumped out head on streams, and the need for the crates async-streams, and parallel-streams.
In our experience some
*Iteratorterminology would have helped clarify what streams are.Given the need for the crates we cited, especially the async-streams RFC, we wonder if there isn't a need for:
IteratorConcurrentIteratorParallelIterator
Or is the intention that async-stream and parallel-stream crate efforts all able to converge into a
streamthat covers the concurrent and parallel use cases?Begley Brothers (Development) at 2021-08-17 22:14:20
Rayon provides a whole ecosystem of parallel iterators on top of a work-stealing threadpool, and is currently the de facto Rust standard for parallel iteration. But I don't foresee a parallel iterator trait getting into the Rust standard library anytime soon.
Streams are supposed to cover the use case of concurrent iterators only (per my understanding). Hopefully once streams are stabilized into the Rust standard library we can have some syntax to use them in concurrent for loops just like we currently use synchronous iterators in for loops. However, there are still some issues to work out like what to do if a a stream panics or is dropped. Settling on a name (Stream vs AsyncIterator vs ConcurrentIterator) will be the easy part! :stuck_out_tongue_winking_eye:
Unfortunately, it's difficult to combine parallel and concurrent iteration at the moment. This would require Rayon to support a way to move tasks from worker threads to an async executor thread, or for an async executor like Tokio to support parallel thread pools in a more sophisticated way than
tokio::task::spawn_blocking(). Until that happens, most programmers try to get all their data into memory first on a concurrent executor-driven threadpool and then offload the synchronous computation to a parallel threadpool (e.g. managed by Rayon).Benjamin Kay at 2021-08-17 22:29:03
multiple people observed that something like
AsyncIteratoror similar might be much more evocative for new users. Such a name would allow people to map their existing understanding of iterators. "I understandIterator, and I understand async, and this is an async version ofIterator".I've filed a PR to the RFCs repo, updating the "streams" terminology to "async iterator" instead: https://github.com/rust-lang/rfcs/pull/3208.
Yosh at 2021-12-14 13:30:14
why did
.nextmake it. where can i get more info on this ?noel zubin victor at 2022-01-05 11:06:00
One potential concern re: methods on
AsyncIterator, which is less of a concern and more of a justification for requiring them, is methods that use internal versus external iteration.For example, it can be very efficient to perform a
foldon an iterator which is composed of severalchains, but it can be much less efficient to directly callnexton said iterators.My concern is that without parity between the methods on
IteratorandAsyncIterator, much of these optimisations that already exist forIteratorwill be lost in the conversion toAsyncIterator. Since, as it stands, there's not really a way to run afor_eachorfoldon a regular iterator if the loop contains async operations.I know for a fact that the current async ecosystem with
futures::stream::Streamdoes not have parity withIterator, with some notable surprises including the fact thattry_foldrequires the iterator item to also be composed of results, rather than just the return type of the function.It would be extremely detrimental to the idea that "this is just the async version of an iterator" if there weren't parity there IMHO, since users might notice slowdowns in code that simply calls
std::async_iter::from_iterwithout adding any extra async code at all.Clar Fon at 2022-02-24 07:12:45
@clarfonthey yes, definitely. The concerns you raise are valid, and the working group is currently actively investigating how we can ensure parity between sync and async Rust APIs. We don't yet (but should) have guidelines on how methods on async traits should be translated from sync to async, but that likely needs us to land async closures / async traits first.
Yosh at 2022-02-24 10:06:51
Given the current trend of the async working group, I wouldn't be surprised if this can become
async fn nextrather thanfn poll_next.Josh Triplett at 2022-06-20 17:22:12
@joshtriplett ah yes, that's definitely something we've been discussing within the working group, and we're currently working towards enabling that. The other thing we're currently researching is keyword-generics, which may allow us to merge the separate
IteratorandAsyncIteratortraits into a singleIteratortrait which is generic over "asyncness".I'll update the tracking issue to reflect both these items.
Yosh at 2022-06-21 10:05:09
(NOT A CONTRIBUTION)
Given the current trend of the async working group, I wouldn't be surprised if this can become async fn next rather than fn poll_next.
ah yes, that's definitely something we've been discussing within the working group, and we're currently working towards enabling that.
I think this is not the right direction for this feature. I hold this view very strongly. AsyncIterator::poll_next enables library authors to write explicit, low-level async iteration primitives for unsafe optimizations. Relying on the compiler generated futures of an async function will make the layout of these types much less predictable or controllable and will take this away from users.
For ease of use where this fine-grained laying control is not desirable, generators are the play (async or otherwise), rather than having to write next methods (async or otherwise) at all.
You need to have the low level APIs (Future::poll, Iterator::next and AsyncIterator::poll_next) for hand-rolled optimized code that the compiler can't be relied on to generate. You need to have the high level syntax (async functions, generators, and async generators) for when people just want to get things done and don't care about these kinds of optimizations. You need both. An async next method would be doing each side of it only halfway (low-level iterator, high-level asynchrony), and would basically trap Rust in a local maxima that looks appealing from where we are now but would not be the best final state.
EDIT: What I mean when I say that "generators are the play" and "local maxima" is that I think because Iterator::next has always existed and has a superficially simple API (ie no Pin, no Context), its not as obvious that for ease of use implementing an iterator with a next method is actually an awful experience for users. Yielding from generators would be much easier. So when you want the ease-of-use story, you want generators, and those can be made async just as easily as functions can. But generators can't guarantee the representation that gets the codegen from for loops over slices looking so good, and similarly won't guarantee the optimizations some async code will want as well. You should be thinking of implementing Iterator::next as really as low-level as implementing Future::poll.
EDIT2: I think the counterargument to this is that mixing-and-matching high-level and low-level is also desirable. IE next + async is desirable when you want control over iteration but don't care about control over asynchrony. Analogously, there must be a hypothetical API that's control over asynchrony but compiler generated iteration - a polling generator(??). I think there could be a case to be made that users do want to be able to drop down into fine control over one aspect of their control flow but not the other, but then that should be an additional, third (and fourth?) option in addition to full control or full ease of use, you can't get rid of the full-control option, which is poll_next.
srrrse at 2023-02-21 12:47:30
For the people that haven't closely followed along on the "blogosphere", I'll link to a few excellent blog posts about it (in chronological order) (authored by people in this thread) (by no means exhaustive):
- https://without.boats/blog/async-iterator/
- https://blog.yoshuawuyts.com/async-iterator-trait/
- https://without.boats/blog/poll-next/
Another argument in favour of
fn poll_nextthat I've not seen explicitly mentioned: we could still provide a defaultasync fn nextimpl, to somewhat improve the user experience of manually callingnext:async fn next(mut self: Pin<&mut Self>) -> Option<Self::Item> { poll_fn(|cx| self.as_mut().poll_next(cx)).await } // Or async fn next(&mut self) -> Option<Self::Item> where Self: Unpin, { poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await }Certainly, this is not as clean as the simple
async fn next(&mut self), see this playground link for an example of how it might be more verbose, because we're allowing the iterator to be self-referential, but might serve to strike enough of a balance?Whether Rust then goes with
self: Pin<&mut Self>orSelf: Unpinmostly depends on what the plans are in the future for makingIteratorable to be self-referential, which I think is still an open question.Mads Marquart at 2023-12-01 00:58:56
@withoutboats for your latest blog post, can you give an approximate loop desugaring like in https://github.com/rust-lang/rust/pull/118847#issue-2036784747 I'm not sure if I'm parsing the ascii charts properly.
Anyway, I'm reraising a concern here that I already mentioned in the PR and that's similar to clarfonthey's:
I think the current proposed interface is terrible for performance when iterating on small types (e.g.
u8s) because thepoll_nextinterface returns aPoll<Option<Item>>that conveys two states at the same time, readiness and end-of-iteration. If the loop body contains no await points, just munching some bytes for example, then that loop body would ideally optimize to a single induction variable based onnext's internals. I.e. just branching on "do we still have more data to process". Only when reaching the end of available data it should poll. This also requires a separation of progress information and getting the next item(s), but it tries to solve a different problem than boat's proposal.One option is to make
poll_nextnot-async (basically justnext) but only return items when the iterator has made progress, which would be polled by a separate method.poll_progresswould then return aPoll<bool>I guess to indicate more items / end of iteration.Another approach is returning an
I where I: IntoIterator<IntoIter=IN>, IN: ExactSizeIteratorfrompoll_next.Optionfulfills these bounds but an async iter that has an internal buffer can instead choose to return some iterable with more than one item, which allows the loop body to process them on a single induction variable of that iterable without polling.My async understanding is limited, I'm coming from the sync Iterators side. So I may have misunderstood something.
the8472 at 2023-12-13 11:14:22
Given the current trend of the async working group, I wouldn't be surprised if this can become
async fn nextrather thanfn poll_next.After rust 1.75 released, trait async func is stable,
async fn nextis more user friendly.jmjoy at 2023-12-30 03:27:14
Given the current trend of the async working group, I wouldn't be surprised if this can become
async fn nextrather thanfn poll_next.After rust 1.75 released, trait async func is stable,
async fn nextis more user friendly.In the rest of the thread, withoutboats has argued that we need async generators for ergonomics, rather than
async fn next(), which is less fine-grained thanfn poll_next()and less ergonomic than async generators in their opinion. Then, what is your rationale for promotingasync fn next()over async generators?Daiki Mizukami at 2023-12-31 02:35:19
Looking at this through the lens of algebraic effects, I would say that
Futurecorresponds to a (suspended ongoing invocation of a) function that can trigger the "pending" effect. The effect has argument and return type()(and eventually the function returnsFuture::Output).Iteratorcorresponds to a (suspended ongoing invocation of a) function that can trigger the "yield" effect. The effect has argument typeIterator::Itemand return type()(and eventually the function returns()).
This analogy is not quite perfect: futures can be polled again after returning "ready" (and similar for iterators); futures have this "context" argument; iterators are not pinned. But I would argue all of those are concessions to how we model these kinds of computations in Rust -- the abstract concept we want to model doesn't have them, but the imperfect realization of that concept in Rust does have them.
An
AsyncIteratorwould then be a (suspended ongoing invocation of a) function that can trigger both the "pending" and the "yield" effect, with the same types as above (and eventually the function returns()).We don't have algebraic effects in Rust, but futures have shown how we can model one particular algebraic effect. If we follow that same paradigm, then the
fn poll_next-based encoding seems to be the most direct way to represent anAyncIterator.In contrast, the
async fn next-based encoding seems to model something different: it represents a function that can trigger a "yield" effect, where the argument type of this effect (i.e., the data being passed from the function to the handler) is a function that can trigger a "pending" effect. If we view an async iterator as triggering a sequence of yield and pending, then this does seem equal in abstract expressivity tofn poll_next(split up the sequence after each yield, to obtain a sequence of subsequences; then each of these subsequences corresponds to one of the functions that is being yielded). However, it is a much less direct encoding of what actually happens, involving a seemingly unnecessary "thunk" (the functions being yielded). Sure, in trivial cases this can be optimized away, but I wouldn't bet much on the claim that such optimizations will always work. It certainly does not seem to fit the usual Rust philosophy of avoiding unnecessary overhead in the most basic abstractions.So I guess what I am saying is, I tend to agree with boats. Mind you, I'm not an expert in async, I am taking a 10,000 foot view of this problem.
Ralf Jung at 2024-02-21 07:09:59
speaking of https://doc.rust-lang.org/nightly/std/async_iter/index.html#laziness --- can the basic async iterator has a usage example? right now these docs show how to define the counter and implement the async iterator trait, but not how to actually invoke it or use it as an end user.
is this because to run this async iterator we need externals like tokio? if so, how do we ensure std has what it needs to run async iterators without needing an external runtime
here's the part i mean, the code isn't really showing what to do with the counter, so it's hard to look at this page of rustdoc and know fully how to get rolling with a minimalist async iterator, which tbh is likely to be much more rewarding for all of us than futures, just a hunch, async iterators are the move, because they promote categorical thinking [about i/o] and especially for input streams
seems like a no brainer to have the example here be a perfectly optimized copypasta for std-only async iterator over some particular user defined type of messages from a generic tcp stream output buffer of bytes, for example
as an aside, i wonder if, in the context of async, some subtle fundamental limitation in
Poll<T>causesPin<&mut self>Which makes me wonder if
pub enum Ratchet<A, B> { Pending(A), Ready(B), }Could give us a different design avenue for async/await besides
Poll<B>if I make sense.Ratchet<A, B>::try_forwardcould be a better option thanPoll<B>withPin<&mut A>because
the
Ratchet::Pending(A)can take the place ofPin<&mut A>and be potentially better aligned with necessity to preserve referenceability of the owned pending state (giving it the term "A" instead of the disembodied "self" on some other detached thing)but maybe
Ratchet<A, B>is too tightly coupled between the "self" of the "Future" (A) and theFuture::Output(B)? just thinking out loud, sorry, carry onbion howard at 2024-05-06 14:29:38
@rustbot labels -I-libs-api-nominated +T-lang +WG-async
This was discussed briefly in the libs-api call today. This work is going to be driven from the lang side on the basis of work done in WG-async, as we had discussed in the meeting on 2024-05-14. On that basis, the decision was to unnominate.
Travis Cross at 2024-05-28 15:44:17
meeting on 2024-05-14
Where can I find the minutes of that meeting? In the t-lang/meetings Zulip channel, I can only find the meeting on 2024-05-15, but that was about a different topic (match ergonomics).
EDIT: Oh, that was a libs-api meeting, not a lang meeting. oops
Ralf Jung at 2024-05-28 16:40:59
One thing from Yosh's blog post that hasn't been mentioned here is that
poll_next()andasync fn next()have slightly different semantics, in thatpoll_next()requires the iterator to be pinned whileasync fn next()doesn't. Can't say for sure which one is better. I've never actually had to move an iterator after callingnext()on it, but maybe that's a legitimate use case.Another thing is that there's a lot of comparisons being made about the performance of
poll_next()versusasync fn next(). The easiest way to resolve this would be to implement an async iterator using both APIs, iterate over them using whateverforloop desugaring we're planning to ship, and compare the compiled assembly/LLVM IR. We should do this for non-trivial iterators and also mix in some combinators. Specifically, we can compare the compiled output of:fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<T>> { // Do something // Return Poll<Option<T>> }with the output of:
async fn next(&mut self) -> Option<T> { future::poll_fn(|cx| /* -> Poll<Option<T>> */ { // Do something // Return Poll<Option<T>> }).await }Since
future::poll_fnallows async functions to "drop down" to lower level polling operations, we can put (mostly) the same code in the bodies of both implementations so that they perform the same operations, leading to a fair comparison. As a bonus, this experiment can also discover perf issues with the loop desugaring and combinator implementations.Yuhan Lin at 2024-11-01 04:02:40
poll_next() requires the iterator to be pinned while async fn next() doesn't
This makes poll_next more powerful. A Stream like https://docs.rs/tokio-util/latest/tokio_util/codec/struct.FramedRead.html currently works with any
! UnpinAsyncRead type. If we moved toasync fn next(), the AsyncRead would need to be Unpin.Similar story is for https://docs.rs/futures-concurrency/latest/futures_concurrency/stream/trait.Merge.html which would now need to cache the futures of each
async fn next(). Since they are all!Unpin, Merge is now forced to box these next futuresConrad Ludgate at 2024-11-01 07:53:37
Similar story is for https://docs.rs/futures-concurrency/latest/futures_concurrency/stream/trait.Merge.html which would now need to cache the futures of each async fn next(). Since they are all !Unpin, Merge is now forced to box these next futures
The main issue here is that with
poll_next(), the iterator itself must contain the async state machine. Whereas withasync fn next(), the async state machine is transiently created by the iterator. Self-contained state machines are much easier to cache and poll in an arbitrary order, which is whatMergedoes. Managing transient state machines created by async functions is a pain point of async traits in general.Yuhan Lin at 2024-11-01 23:56:40
Similar story is for https://docs.rs/futures-concurrency/latest/futures_concurrency/stream/trait.Merge.html which would now need to cache the futures of each
async fn next(). Since they are all!Unpin, Merge is now forced to box these next futuresI would be hesitant to take this as an inherent constraint. I'm fairly certain that with unsafe binders we can formulate an implementation of
Mergebased onasync fn nextwhich does not require any allocations. We'll need to play around a little with the traits and bounds, but I believe it should be possible.Unsafe binders are going to be a great help for async traits regardless of their shape. They are the same feature required to e.g. make
fn poll_nextwork with async closures, which is something we need for example forAsyncIterator::filter.Yosh at 2024-11-03 14:02:55
If we use
async fn next,Mergewill have to store the futures generated by each call tonext. That meansMergewill need to be pinned. I don't see how unsafe lifetime binders can get rid of the pinning requirement. The example of unsafe binders from the design doc also usesPin<&mut Self>.Yuhan Lin at 2024-11-04 06:02:08
I believe there might be a miscommunication: I didn't mean to comment on the pinning requirement, I meant to say it shouldn't be necessary to allocate. While immovability may be inherent to self-references, allocation should not be.
Yosh at 2024-11-04 15:06:53
Right. I suggested box-pinning because at present the
async fn next(&mut self)does not present a pinning interface. That is unless we expect to implementimpl AsyncIterator for Pin<&mut Merge<...>> { ... }otherwise we'd need to instead offer
trait AsyncIterator { async fn next(self: Pin<&mut Self>) -> Option<Self::Item>; }Conrad Ludgate at 2024-11-05 11:08:16
I've been thinking about this problem a bit lately, and I think I have an objection to
async fn nextthat I haven't seen raised before. There are some very basic uses ofasync gen fnthat I don't thinkasync fn nextcan accommodate (EDIT: it actually can):async gen fn shared_state() yield i32 { let reused = foo().await; yield bar(reused).await; yield baz(reused).await; }I can't imagine how that could possibly compile with
async fn next:reusedmust be part of the iterator's state machine to be used on later iterations, but the iterator's state machine can't contain anything that requires await. It can't be part of the transient future's state machine, cause there's no way for the future's state machine to communicate back to the iterator's state machine. I can't see a way around that. EDIT: actually there is an obvious way around that lol, the future's state machine does indeed carry a reference to the iterator state machine and thus can communicate back. My bad! However, this means that the two state machines are effectively one state machine - the future state machine has different states depending on the state of the iterator's state machine. Why not skip the redundancy and just let it be one state machine?This is a direct consequence of Ralph Jung's observation that
poll_nextis the correct effect-based equivalent toasync gen. Anasync gen fnis a function that can yield items and also await futures, not one that yields items that can then be awaited. Or, put another way, we shouldn't think ofAsyncIteratoras adding theasynceffect toIterator, we should think of it as adding theasyncandgeneffects toFnMut(except that it's pinned, soFnPin).I'm not an async Rust expert, so I could be wrong. But it seems like from both a theoretical and practical view,
poll_nextis the correct signature. It's less ergonomic in the short term thanasync fn next, but WAY more expressive, and essentially necessary forasync gento be useful.21aslade at 2025-03-15 14:34:14
Tracking issues aren't meant for discussion of this kind. From above:
A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature.
If you want to discuss this generally, probably the right thing to do would be to open a thread in Zulip under
#wg-async.Travis Cross at 2025-03-15 23:32:22