AsyncSequence & AsyncStream Tutorial for iOS
Learn how to use Swift concurrency’s AsyncSequence and AsyncStream protocols to process asynchronous sequences. By Audrey Tam.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
AsyncSequence & AsyncStream Tutorial for iOS
20 mins
AsyncStream: Push-based
The other kind of AsyncStream
has a build
closure. It creates a sequence of values and buffers them until someone asks for them. Think of it as push-based or supply-driven.
Add this method to ActorAPI
:
// AsyncStream: push-based
func pushActors() async {
// 1
let actorStream = AsyncStream<Actor> { continuation in
// 2
Task {
for try await line in url.lines {
let name = line.components(separatedBy: "\t")[1]
// 3
continuation.yield(Actor(name: name))
}
// 4
continuation.finish()
}
}
for await actor in actorStream {
await MainActor.run {
actors.append(actor)
}
}
}
Here’s what you’re doing in this method:
- You don’t need to create an iterator. Instead, you get a
continuation
. - The
build
closure isn’t asynchronous, so you must create aTask
to loop over the asynchronous sequenceurl.lines
. - For each line, you call the continuation’s
yield(_:)
method to push theActor
value into the buffer. - When you reach the end of
url.lines
, you call the continuation’sfinish()
method.
build
closure isn’t asynchronous, you can use this version of AsyncStream
to interact with non-asynchronous APIs like fread(_:_:_:_:)
.
In ContentView
, call pushActors()
instead of pullActors()
:
await model.pushActors()
Build and run and confirm that it works.
Since Apple first introduced Grand Central Dispatch, it has advised developers on how to avoid the dangers of thread explosion.
When there are more threads than CPUs, the scheduler timeshares the CPUs among the threads, performing context switches to swap out a running thread and swap in a blocked thread. Every thread has a stack and associated kernel data structures, so context-switching takes time.
When an app creates a very large number of threads — say, when it’s downloading hundreds or thousands of images — the CPUs spend too much time context-switching and not enough time doing useful work.
In the Swift concurrency system, there are at most only as many threads as there are CPUs.
When threads execute work under Swift concurrency, the system uses a lightweight object known as a continuation to track where to resume work on a suspended task. Switching between task continuations is much cheaper and more efficient than performing thread context switches.
When a task suspends, it captures its state in a continuation. Its thread can resume execution of another task, recreating its state from the continuation it created when it suspended. The cost of this is a function call.
This all happens behind the scenes when you use async
functions.
But you can also get your hands on a continuation to manually resume execution. The buffering form of AsyncStream
uses a continuation to yield
stream elements.
A different continuation API helps you reuse existing code like completion handlers and delegate methods. To see how, check out Modern Concurrency in Swift, Chapter 5, “Intermediate async/await & CheckedContinuation”.
Push or Pull?
Push-based is like a factory making clothes and storing them in warehouses or stores until someone buys them. Pull-based is like ordering clothes from a tailor.
When choosing between pull-based and push-based, consider the potential mismatch with your use case:
- Pull-based (unfolding)
AsyncStream
: Your code wants values faster than the asynchronous sequence can make them. - Push-based (buffering)
AsyncStream
: The asynchronous sequence generates elements faster than your code can read them, or at irregular or unpredictable intervals, like updates from background monitors — notifications, location, custom monitors
When downloading a large file, a pull-based AsyncStream
— downloading more bytes only when your code asks for them — gives you more control over memory and network use. A push-based AsyncStream
— downloading the whole file without pausing — could create spikes in memory or network use.
To see another difference between the two kinds of AsyncStream
, see what happens if your code doesn’t use actorStream
.
In ActorAPI
, comment out this code in both pullActors()
and pushActors()
:
for await actor in actorStream {
await MainActor.run {
actors.append(actor)
}
}
Next, place breakpoints at this line in both methods:
let name = line.components(separatedBy: "\t")[1]
Edit both breakpoints to log the breakpoint name and hit count, then continue:
Now, in ContentView
, set task
to call pullActors()
:
.task {
await model.pullActors()
}
Build and run, then open the Debug console:
No log messages appear because the code in the pull-based actorStream
doesn’t run when your code doesn’t ask for its elements. It doesn’t read from the file unless you ask for the next element.
Now, switch the task
to call pushActors()
:
.task {
await model.pushActors()
}
Build and run, with the Debug console open:
The push-based actorStream
runs even though your code doesn’t ask for any elements. It reads the entire file and buffers the sequence elements.
Where to Go From Here?
Download the final project using the Download Materials button at the top or bottom of the tutorial.
In this tutorial, you:
- Compared the speed and memory use when synchronously and asynchronously reading a very large file.
- Created and used a custom
AsyncSequence
. - Created and used pull-based and push-based
AsyncStream
s. - Showed that the pull-based
AsyncStream
does nothing until the code asks for sequence elements, while the push-basedAsyncStream
runs whether or not the code asks for sequence elements.
You can use AsyncSequence
and AsyncStream
to generate asynchronous sequences from your existing code — any closures that you call multiple times, as well as delegate methods that just report new values and don’t need a response back. You’ll find examples in our book Modern Concurrency in Swift.
Additional Resources:
- Apple’s AsyncSequence documentation
- WWDC21 Meet AsyncSequence
- WWDC21 Swift Concurrency: Behind the Scenes
- async/await in SwiftUI
- Modern Concurrency in Swift
- SwiftUI and Structured Concurrency
If you have any comments or questions, feel free to join in the forum discussion below!