Lesson 28 of 44
Streams
A stream is the async version of an iterator: a value that produces zero or more items over time, and each item may take an .await to arrive. After this lesson you can create streams from iterators, channels, timers, and generators, transform them with adapters, merge several of them, run async work over their items with bounded concurrency, and reason about cancel safety and backpressure.
| Item | Crate | Role |
|---|---|---|
Stream (poll_next) |
futures-core, re-exported by tokio-stream and futures |
the trait |
StreamExt |
tokio-stream or futures (two different traits) |
adapter methods |
ReceiverStream, IntervalStream, BroadcastStream, WatchStream, LinesStream |
tokio-stream::wrappers |
turn Tokio types into streams |
StreamMap |
tokio-stream |
many keyed streams in one |
stream::unfold, buffered, buffer_unordered, for_each_concurrent |
futures |
building and concurrent processing |
stream!, try_stream! |
async-stream |
generator syntax |
The examples use these crates:
[dependencies]
tokio = { version = "1", features = ["full", "test-util"] }
tokio-stream = "0.1"
futures = "0.3"
async-stream = "0.3"tokio-stream needs its default time feature for timeout, throttle, and chunks_timeout. Examples that involve time use the paused clock (start_paused = true), which makes sleeps finish instantly and in a fixed order. Time: sleep, timeout, and interval explains it.
Example 1: The Stream trait
Stream has one required method, poll_next. It is the Iterator::next of async code. The method returns Poll<Option<Item>>:
| Return value | Meaning |
|---|---|
Poll::Ready(Some(item)) |
the next item |
Poll::Ready(None) |
the stream is finished |
Poll::Pending |
no item yet; the stream has registered the task's waker and will wake it when an item is ready |
Poll, Context, Waker, and Pin come from Polling, Wakers, and a Tiny Executor. This stream never returns Pending, so it never needs the waker. Writing Futures and Streams by Hand covers streams that do.
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio_stream::{Stream, StreamExt};
struct Countdown {
remaining: u32,
}
impl Stream for Countdown {
type Item = u32;
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<u32>> {
if self.remaining == 0 {
return Poll::Ready(None);
}
let value = self.remaining;
self.remaining -= 1;
Poll::Ready(Some(value))
}
}
#[tokio::main]
async fn main() {
let mut countdown = Countdown { remaining: 3 };
while let Some(n) = countdown.next().await {
println!("{n}");
}
println!("done");
}3
2
1
donenext is the StreamExt method that turns one poll_next call into a future. A stream is not usable with for, because for calls a synchronous next. The loop form is while let Some(item) = stream.next().await.
Example 2: Streams from iterators, and single-item streams
tokio_stream::iter wraps any IntoIterator as a stream that is always ready. once yields one item. empty yields nothing. pending never yields and never ends.
use std::time::Duration;
use tokio_stream::{self as stream, StreamExt};
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let mut numbers = stream::iter(vec![10, 20, 30]);
while let Some(n) = numbers.next().await {
println!("iter: {n}");
}
let mut one = stream::once("single");
println!("once: {:?}", one.next().await);
println!("once again: {:?}", one.next().await);
let mut nothing = stream::empty::<u8>();
println!("empty: {:?}", nothing.next().await);
let mut never = stream::pending::<u8>();
let waited = tokio::time::timeout(Duration::from_secs(1), never.next()).await;
println!("pending timed out: {}", waited.is_err());
}iter: 10
iter: 20
iter: 30
once: Some("single")
once again: None
empty: None
pending timed out: truetokio::time::timeout cancels the pending stream's next future after one virtual second. next is cancel safe, so cancelling it loses nothing (Example 23).
Example 3: Adapters that change items
Adapters take a stream and return a new stream. Nothing runs until the final stream is polled. map, filter, filter_map, take, and skip mirror the Iterator methods of the same names.
use tokio_stream::{self as stream, StreamExt};
#[tokio::main]
async fn main() {
let mut s = stream::iter(1..=10)
.skip(1)
.filter(|n| n % 2 == 0)
.map(|n| n * n)
.take(3);
while let Some(n) = s.next().await {
println!("{n}");
}
let mut parsed = stream::iter(["7", "x", "9"]).filter_map(|s| s.parse::<i32>().ok());
while let Some(n) = parsed.next().await {
println!("parsed {n}");
}
}4
16
36
parsed 7
parsed 9The closures are synchronous. To run an async operation for each item, use then (Example 4).
Example 4: then, take_while, and map_while
then takes a closure that returns a future. The stream yields the future's output, and processes one item at a time, in order. take_while ends the stream at the first item that fails a test. map_while maps items and ends the stream when the closure returns None.
use std::time::Duration;
use tokio::time::sleep;
use tokio_stream::{self as stream, StreamExt};
async fn lookup(id: u32) -> String {
sleep(Duration::from_millis(10)).await;
format!("user-{id}")
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let mut names = stream::iter([1, 2, 3]).then(lookup);
while let Some(name) = names.next().await {
println!("{name}");
}
let small: Vec<u32> = stream::iter([1, 2, 3, 10, 4])
.take_while(|n| *n < 5)
.collect()
.await;
println!("take_while: {small:?}");
let digits: Vec<u32> = stream::iter(["4", "5", "x", "6"])
.map_while(|s| s.parse().ok())
.collect()
.await;
println!("map_while: {digits:?}");
}Example 5: Consuming a stream
Consumers return a future instead of a stream: collect, fold, all, any, and next. collect builds a Vec, String, VecDeque, LinkedList, (), or a Result or Option of one of those. Collecting into Result stops at the first Err.
use tokio_stream::{self as stream, StreamExt};
#[tokio::main]
async fn main() {
let total = stream::iter(1..=4).fold(0, |acc, n| acc + n).await;
println!("fold: {total}");
let mut s = stream::iter([2, 4, 6]);
println!("all even: {}", s.all(|n| n % 2 == 0).await);
let mut s = stream::iter([1, 3, 5]);
println!("any even: {}", s.any(|n| n % 2 == 0).await);
let text: String = stream::iter(["ab", "cd", "ef"]).collect().await;
println!("string: {text}");
let ok: Result<Vec<i32>, String> = stream::iter([Ok(1), Ok(2), Ok(3)]).collect().await;
println!("ok: {ok:?}");
let bad: Result<Vec<i32>, String> = stream::iter([Ok(1), Err("bad".to_string()), Ok(3)])
.collect()
.await;
println!("bad: {bad:?}");
let mut results = stream::iter([Ok(1), Err("boom"), Ok(3)]);
println!("try_next: {:?}", results.try_next().await);
println!("try_next: {:?}", results.try_next().await);
}fold: 10
all even: true
any even: false
string: abcdef
ok: Ok([1, 2, 3])
bad: Err("bad")
try_next: Ok(Some(1))
try_next: Err("boom")all and any take &mut self, so the stream stays usable afterwards. try_next returns Result<Option<T>, E> for a stream of Result<T, E>, which allows ? in a loop: while let Some(item) = stream.try_next().await? { ... }.
Example 6: Which crate provides which method
Two crates define a trait called StreamExt: tokio_stream::StreamExt and futures::StreamExt. They have overlapping and different methods. The table below was built by reading tokio-stream 0.1.19 (src/stream_ext.rs) and futures-util 0.3.34 (src/stream/stream/mod.rs).
| Method | tokio-stream |
futures |
|---|---|---|
next, map, filter, filter_map, then, take, skip, take_while, skip_while, fuse, chain, fold, collect, all, any, peekable |
yes | yes |
map_while |
yes | no |
merge |
yes | no (futures::stream::select is the equivalent function) |
timeout, timeout_repeating |
yes | no |
throttle |
yes | no |
chunks_timeout |
yes | no |
try_next |
yes (on StreamExt) |
yes, but on TryStreamExt |
buffered, buffer_unordered |
no | yes |
for_each, for_each_concurrent |
no | yes |
enumerate, zip, scan, inspect, count, cycle, concat, unzip |
no | yes |
flatten, flat_map, flatten_unordered |
no | yes |
chunks, ready_chunks |
no | yes |
take_until, catch_unwind, by_ref, boxed, forward, split |
no | yes |
The next example shows what happens when both traits are in scope.
Example 7: Importing both StreamExt traits is ambiguous
When both traits are imported, a call such as .next() matches two methods, and the compiler rejects it.
use futures::StreamExt;
use tokio_stream::StreamExt as _;
#[tokio::main]
async fn main() {
let mut s = tokio_stream::iter([1, 2, 3]);
println!("{:?}", s.next().await);
}error[E0034]: multiple applicable items in scope
--> src/main.rs:7:24
|
7 | println!("{:?}", s.next().await);
| ^^^^ multiple `next` found
|
= note: candidate #1 is defined in an impl of the trait `tokio_stream::StreamExt` for the type `St`
= note: candidate #2 is defined in an impl of the trait `futures::StreamExt` for the type `T`
help: disambiguate the method for candidate #1
|
7 - println!("{:?}", s.next().await);
7 + println!("{:?}", tokio_stream::StreamExt::next(&mut s).await);
|
help: disambiguate the method for candidate #2
|
7 - println!("{:?}", s.next().await);
7 + println!("{:?}", futures::StreamExt::next(&mut s).await);
|The usual arrangement is one StreamExt per file. Import tokio_stream::StreamExt when the file needs timeout, throttle, merge, or chunks_timeout. Import futures::StreamExt when it needs buffer_unordered, for_each_concurrent, enumerate, or zip. A file that needs both can import one under as _ and call the other with a path, such as tokio_stream::StreamExt::timeout(stream, duration) (Example 26).
Example 8: Methods that only futures provides
These calls have no tokio-stream equivalent.
use futures::{StreamExt, stream};
#[tokio::main]
async fn main() {
let letters = stream::iter(["a", "b", "c"]);
let numbered: Vec<(usize, &str)> = letters.enumerate().collect().await;
println!("{numbered:?}");
let pairs: Vec<(i32, char)> = stream::iter([1, 2, 3])
.zip(stream::iter(['x', 'y']))
.collect()
.await;
println!("{pairs:?}");
let running: Vec<i32> = stream::iter([1, 2, 3, 4])
.scan(0, |sum, n| {
*sum += n;
futures::future::ready(Some(*sum))
})
.collect()
.await;
println!("{running:?}");
stream::iter(["x", "y"])
.for_each(|s| async move { println!("each {s}") })
.await;
}[(0, "a"), (1, "b"), (2, "c")]
[(1, 'x'), (2, 'y')]
[1, 3, 6, 10]
each x
each yzip ends when the shorter stream ends. scan threads mutable state through the items; its closure returns a future.
Example 9: fuse
The Stream contract does not say what happens if poll_next is called again after it returned None. A stream may return Some again, panic, or return None forever. fuse guarantees None forever after the first None. Adapters such as merge and timeout fuse their inputs internally.
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio_stream::{Stream, StreamExt};
struct Flaky {
step: u32,
}
impl Stream for Flaky {
type Item = u32;
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<u32>> {
self.step += 1;
match self.step {
1 => Poll::Ready(Some(1)),
2 => Poll::Ready(None),
n => Poll::Ready(Some(n)),
}
}
}
#[tokio::main]
async fn main() {
let mut raw = Flaky { step: 0 };
println!("raw: {:?} {:?} {:?}", raw.next().await, raw.next().await, raw.next().await);
let mut fused = Flaky { step: 0 }.fuse();
println!("fused: {:?} {:?} {:?}", fused.next().await, fused.next().await, fused.next().await);
}raw: Some(1) None Some(3)
fused: Some(1) None NoneExample 10: timeout
timeout(duration) wraps each item in Result<T, Elapsed>. The deadline applies to each item. When it passes, the stream yields Err(Elapsed) and continues to poll the inner stream, so a later item still arrives. tokio_stream::Elapsed is the error type.
use std::time::Duration;
use tokio::time::sleep;
use tokio_stream::StreamExt;
fn delayed() -> impl tokio_stream::Stream<Item = &'static str> {
async_stream::stream! {
sleep(Duration::from_millis(50)).await;
yield "fast";
sleep(Duration::from_millis(500)).await;
yield "slow";
}
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let stream = delayed().timeout(Duration::from_millis(100));
tokio::pin!(stream);
while let Some(item) = stream.next().await {
match item {
Ok(value) => println!("got {value}"),
Err(_) => println!("timed out waiting for an item"),
}
}
}got fast
timed out waiting for an item
got slowTo stop at the first timeout instead, add .take_while(Result::is_ok). timeout_repeating(interval) takes an Interval and yields Err(Elapsed) on every tick that passes without an item. The stream is built with async_stream::stream!, explained in Example 19. tokio::pin! is needed because that stream is not Unpin.
Example 11: chunks_timeout
chunks_timeout(max_size, duration) groups items into Vecs. A batch is emitted when it reaches max_size, when duration has passed since the first item of the batch, or when the stream ends. Empty batches are never emitted. It panics if max_size is zero.
use std::time::Duration;
use tokio::time::sleep;
use tokio_stream::StreamExt;
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let source = async_stream::stream! {
for n in 1..=4 {
yield n;
}
sleep(Duration::from_secs(5)).await;
yield 5;
yield 6;
};
let batches = source.chunks_timeout(3, Duration::from_secs(2));
tokio::pin!(batches);
while let Some(batch) = batches.next().await {
println!("{batch:?}");
}
}[1, 2, 3]
[4]
[5, 6]Items 1 to 3 fill a batch. Item 4 waits alone until the two-second deadline. Items 5 and 6 arrive after the pause and are flushed when the stream ends. Batching like this reduces the number of database writes or network calls when items arrive faster than the sink can absorb them one by one.
Example 12: throttle
throttle(duration) enforces a minimum gap between items. The timer has a granularity of one millisecond. Under the paused clock, Instant differences are exact, so the elapsed time can be printed.
use std::time::Duration;
use tokio::time::Instant;
use tokio_stream::{self as stream, StreamExt};
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let start = Instant::now();
let mut s = stream::iter(["a", "b", "c"]).throttle(Duration::from_millis(200));
while let Some(item) = s.next().await {
println!("{item} at {} ms", start.elapsed().as_millis());
}
}Example 13: merge
merge combines two streams into one that yields items as either source produces them. The merged stream ends when both sources have ended. When both are ready, merge alternates which source it polls first, so neither can starve the other.
use std::time::Duration;
use tokio::time::sleep;
use tokio_stream::{Stream, StreamExt};
fn ticker(name: &'static str, every_ms: u64, count: u32) -> impl Stream<Item = String> {
async_stream::stream! {
for i in 1..=count {
sleep(Duration::from_millis(every_ms)).await;
yield format!("{name}{i}");
}
}
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let merged = ticker("a", 30, 3).merge(ticker("b", 50, 2));
tokio::pin!(merged);
while let Some(item) = merged.next().await {
println!("{item}");
}
}a1
b1
a2
a3
b2The order follows the virtual arrival times: a1 at 30 ms, b1 at 50 ms, a2 at 60 ms, a3 at 90 ms, b2 at 100 ms. To merge more than two streams, chain merge calls, use futures::stream::select_all, or use StreamMap (Example 18) when the consumer must know which source produced each item.
Example 14: ReceiverStream
tokio_stream::wrappers::ReceiverStream turns an mpsc::Receiver into a stream. All adapters then apply to a channel. UnboundedReceiverStream does the same for an unbounded channel. The stream ends when every Sender is dropped and the buffer is empty. mpsc Channels covers the channel.
use tokio::sync::mpsc;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::ReceiverStream;
#[tokio::main]
async fn main() {
let (tx, rx) = mpsc::channel(4);
tokio::spawn(async move {
for n in 1..=5 {
tx.send(n).await.unwrap();
}
});
let doubled: Vec<i32> = ReceiverStream::new(rx).map(|n| n * 2).collect().await;
println!("{doubled:?}");
}[2, 4, 6, 8, 10]Example 15: IntervalStream and WatchStream
IntervalStream yields an Instant on every tick of a tokio::time::Interval. WatchStream::new yields the current value of a watch channel first and then each change. WatchStream::from_changes skips the current value. A watch channel keeps only the latest value, so a slow consumer of a WatchStream sees fewer items than were sent. oneshot, broadcast, and watch covers the channel.
use std::time::Duration;
use tokio::sync::watch;
use tokio::time::interval;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::{IntervalStream, WatchStream};
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let mut ticks = IntervalStream::new(interval(Duration::from_secs(1))).take(3);
let mut count = 0;
while ticks.next().await.is_some() {
count += 1;
println!("tick {count}");
}
let (tx, rx) = watch::channel("initial");
let mut with_current = WatchStream::new(rx.clone());
let mut changes_only = WatchStream::from_changes(rx);
println!("with_current: {:?}", with_current.next().await);
tx.send("v1").unwrap();
tx.send("v2").unwrap();
println!("with_current: {:?}", with_current.next().await);
println!("changes_only: {:?}", changes_only.next().await);
}Two sends happened before either stream polled again, so both streams saw only v2.
Example 16: BroadcastStream and lag
BroadcastStream yields Result<T, BroadcastStreamRecvError>. When the receiver falls behind by more than the channel capacity, the next item is Err(Lagged(n)), where n is the number of skipped messages. After that, the stream continues with the oldest retained message.
use tokio::sync::broadcast;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
#[tokio::main]
async fn main() {
let (tx, rx) = broadcast::channel(2);
for n in 1..=5 {
tx.send(n).unwrap();
}
drop(tx);
let mut s = BroadcastStream::new(rx);
while let Some(item) = s.next().await {
match item {
Ok(n) => println!("got {n}"),
Err(e) => println!("error: {e}"),
}
}
}Example 17: LinesStream
LinesStream wraps the Lines value from BufReader::lines() (see AsyncRead and AsyncWrite) so that adapters apply to lines of text. Its items are io::Result<String>.
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio_stream::StreamExt;
use tokio_stream::wrappers::LinesStream;
#[tokio::main]
async fn main() {
let data: &[u8] = b"alpha\nbeta\n\ngamma\n";
let lines = LinesStream::new(BufReader::new(data).lines());
let non_empty: Vec<String> = lines
.filter_map(|line| line.ok())
.filter(|line| !line.is_empty())
.map(|line| line.to_uppercase())
.collect()
.await;
println!("{non_empty:?}");
}Example 18: StreamMap
StreamMap<K, V> holds many streams under keys and yields (K, item) pairs as items become available. Streams can be inserted and removed while it is being polled. A stream that ends is removed automatically. The map ends when it is empty. It scans all entries on every operation, so it suits a small number of streams.
use std::pin::Pin;
use std::time::Duration;
use tokio::time::sleep;
use tokio_stream::{Stream, StreamExt, StreamMap};
fn numbers(start: u32, gap_ms: u64) -> Pin<Box<dyn Stream<Item = u32>>> {
Box::pin(async_stream::stream! {
for n in start..start + 2 {
sleep(Duration::from_millis(gap_ms)).await;
yield n;
}
})
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let mut map = StreamMap::new();
map.insert("fast", numbers(10, 10));
map.insert("slow", numbers(20, 25));
println!("streams: {}", map.len());
while let Some((key, value)) = map.next().await {
println!("{key} -> {value}");
if key == "slow" && value == 20 {
map.remove("fast");
println!("removed fast, {} left", map.len());
}
}
}streams: 2
fast -> 10
fast -> 11
slow -> 20
removed fast, 1 left
slow -> 21StreamMap needs Unpin streams, so each one is boxed with Box::pin. The fast stream is removed before it finishes, so 11 never appears.
Example 19: Building streams with unfold and async-stream
futures::stream::unfold(state, f) builds a stream from a state value and an async closure. The closure returns Some((item, next_state)) or None to end. async_stream::stream! is generator syntax: the body is an async block in which yield value produces an item. try_stream! allows ? and yields Result items, ending the stream at the first error.
use futures::stream;
use std::pin::pin;
use tokio_stream::StreamExt;
fn parse_all(
inputs: Vec<&'static str>,
) -> impl tokio_stream::Stream<Item = Result<i32, std::num::ParseIntError>> {
async_stream::try_stream! {
for text in inputs {
let n: i32 = text.parse()?;
yield n;
}
}
}
#[tokio::main]
async fn main() {
let fib = stream::unfold((0u64, 1u64), |(a, b)| async move { Some((a, (b, a + b))) });
let first: Vec<u64> = fib.take(8).collect().await;
println!("unfold: {first:?}");
let evens = async_stream::stream! {
for n in 0..10 {
if n % 2 == 0 {
yield n;
}
}
};
let mut evens = pin!(evens);
while let Some(n) = evens.next().await {
println!("stream!: {n}");
}
let mut results = pin!(parse_all(vec!["1", "2", "x", "4"]));
while let Some(item) = results.next().await {
match item {
Ok(n) => println!("ok {n}"),
Err(e) => println!("err {e}"),
}
}
}unfold: [0, 1, 1, 2, 3, 5, 8, 13]
stream!: 0
stream!: 2
stream!: 4
stream!: 6
stream!: 8
ok 1
ok 2
err invalid digit found in stringThe stream! result is not Unpin, so the example pins it with std::pin::pin! before calling next. Box::pin(stream) also works and produces an Unpin value. take in the first part consumes fib by value, so no pinning is needed there: collect takes ownership and pins internally.
Example 20: Returning streams from functions
A function can return impl Stream<Item = T>. To pass the stream to tokio::spawn, the type must also be Send and 'static. Add + Send to the return type to state that promise. Pin<Box<dyn Stream<Item = T> + Send>> is the form for a value that could be one of several stream types.
use std::pin::Pin;
use tokio_stream::{self as stream, Stream, StreamExt};
fn evens() -> impl Stream<Item = u32> + Send {
stream::iter(0..).filter(|n| n % 2 == 0)
}
fn choose(reverse: bool) -> Pin<Box<dyn Stream<Item = u32> + Send>> {
if reverse {
Box::pin(stream::iter([3, 2, 1]))
} else {
Box::pin(stream::iter([1, 2, 3]))
}
}
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
let mut s = evens().take(4);
let mut out = Vec::new();
while let Some(n) = s.next().await {
out.push(n);
}
out
});
println!("{:?}", handle.await.unwrap());
let mut s = choose(true);
while let Some(n) = s.next().await {
println!("boxed {n}");
}
}[0, 2, 4, 6]
boxed 3
boxed 2
boxed 1The boxed stream is Unpin (a Pin<Box<T>> always is), so next works without further pinning.
Example 21: buffered and buffer_unordered
A stream of futures can run those futures concurrently. futures::StreamExt::buffered(n) polls up to n futures at once and yields results in the original order. buffer_unordered(n) yields each result as it completes. Both pull from the source only when they have room, so n is a hard limit on concurrent work.
use futures::StreamExt;
use std::time::Duration;
use tokio::time::sleep;
async fn work(id: u64) -> u64 {
sleep(Duration::from_millis(100 - id * 30)).await;
println!(" finished {id}");
id
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
println!("buffered(3):");
let ordered: Vec<u64> = futures::stream::iter(0..3).map(work).buffered(3).collect().await;
println!(" result order: {ordered:?}");
println!("buffer_unordered(3):");
let unordered: Vec<u64> = futures::stream::iter(0..3)
.map(work)
.buffer_unordered(3)
.collect()
.await;
println!(" result order: {unordered:?}");
}buffered(3):
finished 2
finished 1
finished 0
result order: [0, 1, 2]
buffer_unordered(3):
finished 2
finished 1
finished 0
result order: [2, 1, 0]Task 2 sleeps the shortest time and finishes first in both runs. buffered holds its result until tasks 0 and 1 are also done, then yields 0, 1, 2. buffer_unordered yields 2, 1, 0. The map call only creates the futures; the buffer adapter polls them.
Example 22: Bounded concurrency with for_each_concurrent
for_each_concurrent(limit, f) runs an async closure for each item with at most limit running at once. It returns a future that completes when the stream is exhausted and all closures finished. A limit of None means no limit. The closure's futures all run inside the calling task, not on separate tasks, so they share one thread. JoinSet and Structured Concurrency covers the alternative that spawns tasks.
use futures::StreamExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::time::sleep;
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let running = AtomicUsize::new(0);
let peak = AtomicUsize::new(0);
futures::stream::iter(0..10)
.for_each_concurrent(3, |_id| async {
let now = running.fetch_add(1, Ordering::SeqCst) + 1;
peak.fetch_max(now, Ordering::SeqCst);
sleep(Duration::from_millis(10)).await;
running.fetch_sub(1, Ordering::SeqCst);
})
.await;
println!("peak concurrency: {}", peak.load(Ordering::SeqCst));
}peak concurrency: 3Example 23: Cancel safety of next
StreamExt::next is cancel safe. Its future holds only a reference to the stream. Dropping the future before it completes does not consume an item, so the next call still receives it. This makes next safe as a branch of select! inside a loop. select!: Racing Futures covers the macro.
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::sleep;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::ReceiverStream;
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let (tx, rx) = mpsc::channel(8);
tokio::spawn(async move {
for n in 1..=3 {
sleep(Duration::from_millis(250)).await;
tx.send(n).await.unwrap();
}
});
let mut stream = ReceiverStream::new(rx);
let mut cancelled = 0;
loop {
tokio::select! {
item = stream.next() => match item {
Some(n) => println!("item {n}"),
None => break,
},
_ = sleep(Duration::from_millis(100)) => cancelled += 1,
}
}
println!("next() futures dropped before completing: {cancelled}");
}item 1
item 2
item 3
next() futures dropped before completing: 6The 100 ms branch wins several times per item. Each win drops the pending next() future. All three items still arrive, in order.
Example 24: Streams are lazy and pull-based
An adapter polls its source only when its own consumer asks for an item. A stream that nobody polls does no work. The counter below shows that take(2) on a chain of adapters pulls only as many source items as needed and stops.
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio_stream::{self as stream, StreamExt};
static PULLED: AtomicUsize = AtomicUsize::new(0);
#[tokio::main]
async fn main() {
let source = stream::iter(1..=1_000_000).map(|n| {
PULLED.fetch_add(1, Ordering::SeqCst);
n
});
let pipeline = source.filter(|n| n % 3 == 0).take(2);
println!("pulled before polling: {}", PULLED.load(Ordering::SeqCst));
let items: Vec<i32> = pipeline.collect().await;
println!("items: {items:?}");
println!("pulled after: {}", PULLED.load(Ordering::SeqCst));
}pulled before polling: 0
items: [3, 6]
pulled after: 6Six items were pulled to find the first two multiples of 3. This pull model is the base of backpressure: a slow consumer polls less often, so the producer is asked for less.
Example 25: Backpressure with a bounded channel
A pull-based stream slows its producer only if the producer is connected through something that can say "full". A bounded mpsc channel does that: send(..).await waits when the buffer is full, and try_send returns an error. An unbounded channel never says "full", so a producer faster than its consumer makes memory grow without limit.
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::ReceiverStream;
#[tokio::main]
async fn main() {
let (tx, rx) = mpsc::channel(2);
let mut accepted = 0;
let mut rejected = 0;
for n in 1..=5 {
match tx.try_send(n) {
Ok(()) => accepted += 1,
Err(TrySendError::Full(_)) => rejected += 1,
Err(TrySendError::Closed(_)) => unreachable!(),
}
}
println!("accepted {accepted}, rejected {rejected}");
let mut stream = ReceiverStream::new(rx);
println!("first: {:?}", stream.next().await);
println!("try_send after one read: {:?}", tx.try_send(6).is_ok());
}accepted 2, rejected 3
first: Some(1)
try_send after one read: trueCapacity 2 accepts two items and rejects the other three. Reading one item frees one slot, so the next try_send succeeds. A producer that uses send(..).await would wait instead of failing. Backpressure and Load Shedding builds on this.
Example 26: A pipeline with bounded concurrency and batching
This example combines the tools above: a stream of ids, concurrent lookups with buffer_unordered, and batching with chunks_timeout. Both extension traits are needed, so one is imported anonymously and the other is called by path.
use futures::StreamExt as _;
use std::time::Duration;
use tokio::time::sleep;
async fn fetch(id: u32) -> (u32, u32) {
sleep(Duration::from_millis(u64::from(50 - id * 10))).await;
(id, id * id)
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let results = futures::stream::iter(1..=4).map(fetch).buffer_unordered(2);
let batches = tokio_stream::StreamExt::chunks_timeout(results, 3, Duration::from_secs(1));
tokio::pin!(batches);
while let Some(batch) = batches.next().await {
println!("batch: {batch:?}");
}
}batch: [(2, 4), (1, 1), (3, 9)]
batch: [(4, 16)]use futures::StreamExt as _ brings the trait's methods into scope without a name, so map, buffer_unordered, and next resolve to futures. The tokio-stream method is called by its full path, so there is no ambiguity.
Example 27: Sink, named only
Sink is the write-side counterpart of Stream: a value that accepts items with start_send and pushes them out with poll_flush. It lives in futures-sink, and futures::SinkExt adds send, feed, flush, and close. Framed and FramedWrite in Codecs with tokio-util implement Sink, and that lesson uses SinkExt::send. Nothing else in this lesson needs it.
Interview points
- A
Streamis an async iterator:poll_nextreturnsReady(Some),Ready(None), orPending.forloops do not work with it. The loop iswhile let Some(x) = s.next().await. tokio_stream::StreamExtandfutures::StreamExtare different traits. Importing both makes overlapping methods ambiguous.timeout,throttle,merge,chunks_timeout, andmap_whileexist only intokio-stream.buffered,buffer_unordered,for_each_concurrent,enumerate, andzipexist only infutures.nextrequiresUnpin. Usetokio::pin!,std::pin::pin!, orBox::pinfor streams fromstream!orunfold.nextis cancel safe. It can be aselect!branch in a loop without losing items.buffered(n)preserves input order.buffer_unordered(n)yields completion order. Both limit concurrency ton.for_each_concurrentruns its futures inside one task. UseJoinSetorspawnfor parallelism across threads.StreamMapyields keyed items and drops finished streams silently. Its cost is linear in the number of streams.BroadcastStreamreports lag asErr(Lagged(n)).WatchStreamcoalesces values.- Bounded channels give backpressure. Unbounded channels grow until memory runs out.
fuseguarantees that a finished stream stays finished.
Summary
Stream::poll_nextreturnsPoll<Option<Item>>;Noneends the stream.stream::iter,once,empty, andpendingbuild simple streams.unfoldandasync_stream::stream!build stateful ones.- Adapters are lazy. Nothing runs until the final stream is polled, and each adapter pulls from its source only on demand.
collectbuildsVec,String,VecDeque,LinkedList,(), andResultorOptionof those.timeoutwraps items inResult<T, Elapsed>and continues after a timeout.chunks_timeoutbatches by size or time.throttlespaces items.mergecombines two streams fairly.StreamMapmerges keyed streams that can be added and removed while polling.- Wrappers
ReceiverStream,IntervalStream,BroadcastStream,WatchStream, andLinesStreamconnect Tokio types to the adapter methods. buffer_unorderedandfor_each_concurrentbound concurrent work. Bounded channels bound queued work.Sinkis the write-side trait, used withSinkExtin the codecs lesson.
