await, in fullWhat one await does to a function, how a loop with an await in it is built, what waiting really is, how for await and AsyncStream are made of those, and what an await does to an actor — each with a program that compiles and runs
You have written await since lesson 15, looped with for await since lesson 40, and put code on the main actor and on actors of your own since lesson 37 and lesson 38. Each of those lessons answers its own question. This guide answers one: what does await do? The answer is small — every await is a new function — and once you have it, a loop with an await in it, a continuation, for await, AsyncStream and an actor that lets another call in are all the same thing seen from different sides. Each section adds one thing to the one before, and everything on the page that claims to print something has been compiled and run, built as the course's projects are: Swift 6, nothing on the main actor unless it says so, and no async function following its caller unless it says so.
await is a new function · lesson 37await in it · lesson 40for await: the loop the compiler writes for you · lesson 40AsyncStream · lesson 40Task, and when the task is cancelled · lesson 40await on an actor: the main actor, and one of your own · lesson 38await is a new functionOne async function, with one await in it. answer() takes a moment, as a request does, and main waits for it:
/// An answer that takes a moment, as a request's does. `Task.sleep` holds no thread while it waits.
func answer() async -> Int {
try? await Task.sleep(for: .milliseconds(10))
return 7
}print("before the await")
let n = await answer()
print("after the await:", n)It prints
before the await
after the await: 7Here is the same program written with no async and no await at all. It is what the compiler builds from the one above, and the rule it follows is the whole of this guide: everything before an await stays in one function, and everything after it becomes a new function. answer cannot return a value it does not have yet, so it takes the function to call when it has one:
import Foundation
/// The same answer, with no `async`. It cannot return a value it does not have yet, so it takes the function to
/// call when it has one — and calls it from another thread, as the real one is resumed from another thread.
func answer(thenContinueAt whereToGoNext: @escaping @Sendable (Int) -> Void) {
Thread.detachNewThread {
Thread.sleep(forTimeInterval: 0.01)
whereToGoNext(7)
}
}
/// `main`, as the compiler builds it: everything before the await, and everything after it.
enum Main {
static func start() {
print("before the await")
answer(thenContinueAt: afterTheAnswer)
}
static func afterTheAnswer(_ n: Int) {
print("after the await:", n)
done.signal()
}
}
/// Only keeps the program alive until the second piece has run. An app's run loop does that for it.
let done = DispatchSemaphore(value: 0)Main.start()
done.wait()It prints the same
before the await
after the await: 7start is piece 1: it prints, hands answer the name of piece 2, and returns — the thread is free. afterTheAnswer is piece 2, and it runs when answer calls it, on whatever thread answer was on. The value n never sits in a local variable across the wait, because there is no function left to hold it: it arrives as piece 2's argument. That is what await means: the function ends here, and the rest of it is a new function that the awaited call knows how to reach.
The compiler's own version has four pieces, not two. These are the functions it made of main, from the compiled program, with the names the demangler gives them:
static Program.main() async -> ()
(1) suspend resume partial function for static Program.main() async -> ()
(2) await resume partial function for static Program.main() async -> ()
(3) suspend resume partial function for static Program.main() async -> ()The first is main as far as its first hop: main runs on the main actor, and getting there is a hop. A suspend resume partial function is a piece that begins after a hop; an await resume partial function is one that begins when an awaited call has answered — afterTheAnswer, in the hand-written version. The extra two come from the hops: answer is nonisolated, so it runs on the pool of threads, and when it has answered the task comes back to the main actor before piece 3 goes on. So the count is not of lines but of awaits: one await, and main is four functions. The two you wrote by hand are the ones that carry your code.
await in itNow put the await inside a loop. Countdown hands out three, two, one and then nil, through an async method, and main asks until it gets nil. No protocol anywhere — while let needs only an optional, and the await only an async method:
/// Three, two, one, handed out one at a time. No protocol: an `async` method that answers `nil` when it is done.
struct Countdown {
var left: Int
mutating func next() async -> Int? {
guard left > 0 else {
return nil
}
left -= 1
return left + 1
}
}var iterator = Countdown(left: 3)
while let number = await iterator.next() {
print(number)
}
print("Liftoff")It prints
3
2
1
LiftoffThe same rule as before — everything before the await in one function, everything after it in another — but the await is in the middle of a loop, so the cut goes through the loop, and a loop that has been cut cannot stay a loop. Here is what it becomes. start is the piece before the await. gotAnswer is the piece after it, and the piece after it is the rest of the loop: the nil check, the body, and the next call of next — which is told to come back to gotAnswer:
/// The countdown with no `async`: `next` calls `whereToGoNext` instead of returning.
final class Countdown {
var left: Int
init(left: Int) {
self.left = left
}
func next(thenContinueAt whereToGoNext: (Int?) -> Void) {
guard left > 0 else {
whereToGoNext(nil)
return
}
left -= 1
whereToGoNext(left + 1)
}
}
/// `main`, as the compiler builds it: the piece before the await, and the piece after — which is the rest of the loop.
final class Main {
let iterator = Countdown(left: 3)
let whereToGoNext: () -> Void
/// Whoever starts `main` says where to continue when it is done.
init(thenContinueAt whereToGoNext: @escaping () -> Void) {
self.whereToGoNext = whereToGoNext
}
func start() {
iterator.next(thenContinueAt: gotAnswer)
}
func gotAnswer(_ number: Int?) {
guard let number else {
print("Liftoff")
whereToGoNext() // this is `main` returning
return
}
print(number)
iterator.next(thenContinueAt: gotAnswer)
}
}// Stands in for the code the compiler writes around `main`, which ends the program when `main` is done.
let main = Main(thenContinueAt: {
print("back in the startup code, which ends the program")
})
main.start()It prints the same, and one line more
3
2
1
Liftoff
back in the startup code, which ends the programThree things are worth seeing in it. There is no while left. Going round again is gotAnswer asking next to come back to gotAnswer. The line after the loop needs no function of its own. print("Liftoff") sits in gotAnswer's nil branch, because leaving a loop is the if taking its other branch, not an await; a new function starts only at an await. return is a call. main cannot go back up a stack when it is done, because nothing of it is on a stack; whoever started main said where to continue, and whereToGoNext() is main returning — forward, to the function it was given. For main that is startup code the compiler writes, which ends the program, and the hand-written run stands in for it with one more line.
The compiler's pieces for the loop are the same four as for one await, because the loop has one await:
static Program.main() async -> ()
(1) suspend resume partial function for static Program.main() async -> ()
(2) await resume partial function for static Program.main() async -> ()
(3) suspend resume partial function for static Program.main() async -> ()You can watch the loop disappear. Here is the same countdown with no async — next answers at once — and the machine code the compiler emits for main, cut down to the lines that say where control goes:
/// The same countdown with no `async`: `next` answers at once.
struct Countdown {
var left: Int
mutating func next() -> Int? {
guard left > 0 else {
return nil
}
left -= 1
return left + 1
}
}var iterator = Countdown(left: 3)
while let number = iterator.next() {
print(number)
}
print("Liftoff")static Program.main() -> ():
bl Countdown.init(left: Swift.Int) -> Countdown
b LBB6_1
LBB6_1:
bl Countdown.next() -> Swift.Int?
b.eq LBB6_4
b LBB6_2
LBB6_2:
b LBB6_3
LBB6_3:
bl Swift.print(_: Any..., separator: Swift.String, terminator: Swift.String) -> ()
b LBB6_1
LBB6_4:
bl Swift.print(_: Any..., separator: Swift.String, terminator: Swift.String) -> ()
ret| Instruction | What the CPU does |
|---|---|
bl | What the CPU doesbranch with link: saves the address of the next instruction in a register, then jumps — a call, which ret comes back from |
b | What the CPU doesbranch: jumps, and nothing records where it came from |
b.eq | What the CPU doesbranch if equal: jumps only if the last comparison came out equal — here, if next answered nil |
ret | What the CPU doesjumps to the address bl saved: the function returns |
br x0 | What the CPU doesjumps to the address in a register — whatever was put there |
One function. bl next calls next and comes back to the b.eq; b.eq leaves the loop if the answer was nil; b LBB6_1 jumps back up, and that jump is the while. At the end, ret.
Now the async version, the loop of the last section, in the same cut:
static Program.main() async -> ():
bl type metadata accessor for Swift.MainActor
bl static Swift.MainActor.shared.getter : Swift.MainActor
bl dispatch thunk of Swift.Actor.unownedExecutor.getter : Swift.UnownedSerialExecutor
adrp x0, (1) suspend resume partial function for static Program.main() async -> ()@PAGE
b _swift_task_switch
(1) suspend resume partial function for static Program.main() async -> ():
bl Countdown.init(left: Swift.Int) -> Countdown
adrp x8, async function pointer to Countdown.next() async -> Swift.Int?@PAGE
bl _swift_task_alloc
adrp x8, (2) await resume partial function for static Program.main() async -> ()@PAGE
b Countdown.next() async -> Swift.Int?
(2) await resume partial function for static Program.main() async -> ():
bl _swift_task_dealloc
adrp x0, (3) suspend resume partial function for static Program.main() async -> ()@PAGE
b _swift_task_switch
(3) suspend resume partial function for static Program.main() async -> ():
b.eq LBB10_2
b LBB10_1
LBB10_1:
bl Swift.print(_: Any..., separator: Swift.String, terminator: Swift.String) -> ()
adrp x8, async function pointer to Countdown.next() async -> Swift.Int?@PAGE
bl _swift_task_alloc
adrp x9, (2) await resume partial function for static Program.main() async -> ()@PAGE
b Countdown.next() async -> Swift.Int?
LBB10_2:
bl _swift_release
bl Swift.print(_: Any..., separator: Swift.String, terminator: Swift.String) -> ()
br x0Four functions, and every line that reaches next is b, a one-way jump, never bl: there is no coming back, because the piece that jumped has ended. Before it jumps, a piece writes the address of piece 2 into the frame it has just allocated for next — the adrp of "(2) await resume partial function" — which is thenContinueAt: in the machine's own words; next ends by jumping to that address. Piece 3 ends the same way piece 1 did, with a b to next, and that is what goes round. When next answers nil, piece 3 ends with br x0: a jump to whatever address was put in x0, which is wherever main was told to continue. There is no ret anywhere in it, because nothing of main is on the thread's stack to return to.
So far every await was answered almost at once. The point of the whole arrangement is what happens when it is not. The piece before the await has returned, and the piece after it is a function with an address: so there is nothing on any thread, and the thread goes off to run other tasks. What is left is the task — its frames, on the heap, and a note of which piece to run next. Something has to bring it back, and that something is a continuation: withCheckedContinuation hands the wait a continuation, and whoever calls its resume puts the task back on a thread. Here the countdown waits, before every answer, for another thread to say so:
import Foundation
/// Waits until another thread says so. `withCheckedContinuation` hands the wait a continuation, the way back in;
/// `resume` is the other thread using it.
func waitForTheOtherThread() async {
await withCheckedContinuation { continuation in
Thread.detachNewThread {
Thread.sleep(forTimeInterval: 0.02)
continuation.resume()
}
}
}
/// The countdown, with a real wait before each answer.
struct Countdown {
var left: Int
mutating func next() async -> Int? {
await waitForTheOtherThread()
guard left > 0 else {
return nil
}
left -= 1
return left + 1
}
}var iterator = Countdown(left: 3)
while let number = await iterator.next() {
print(number)
}
print("Liftoff")It prints
3
2
1
LiftoffIn the compiled program, the wait is a call into the Swift runtime. These are the lines of LLVM IR — the compiler's last stage before machine code — that call it:
musttail call swifttailcc void @swift_continuation_await(ptr %…) #3swift_continuation_await marks the task as suspended and returns the thread to the pool; resume, on the other thread, calls swift_continuation_resume, which puts the task back on its executor's queue. Neither holds a thread for the task in between. Here is what that is worth: ten thousand tasks, every one of them waiting, and the number of threads in the process while they do. A small actor, which section 8 is about, keeps their continuations until it is told to let them all go:
import Foundation
/// Keeps every waiting continuation until `open()`, as a stream keeps its reader's.
actor Gate {
private var waiting: [CheckedContinuation<Void, Never>] = []
func wait() async {
await withCheckedContinuation { continuation in
waiting.append(continuation)
}
}
func open() {
let all = waiting
waiting = []
for continuation in all {
continuation.resume()
}
}
}
/// How many threads this process has right now.
func threadCount() -> Int {
var threads: thread_act_array_t?
var count: mach_msg_type_number_t = 0
task_threads(mach_task_self_, &threads, &count)
if let threads {
vm_deallocate(mach_task_self_, vm_address_t(bitPattern: threads), vm_size_t(Int(count) * MemoryLayout<thread_t>.stride))
}
return Int(count)
}let gate = Gate()
print("threads before:", threadCount())
let total = await withTaskGroup(of: Int.self) { group in
for _ in 0..<10_000 {
group.addTask {
await gate.wait()
return 1
}
}
try? await Task.sleep(for: .milliseconds(500))
print("threads while 10,000 tasks wait:", threadCount())
await gate.open()
var total = 0
for await one in group {
total += one
}
return total
}
print("every task got through:", total == 10_000)It prints
threads before: 1
threads while 10,000 tasks wait: 6
every task got through: trueTen thousand waits, and a handful of threads: the pool's one per core, and a few the system keeps. A waiting task costs a frame, not a thread, and that is why an app can have a .task waiting in every view on the screen and never run out of anything. It is also why the pool's threads must never be blocked with a lock or a sleep that holds the thread — section 9 measures what happens when one is.
for await: the loop the compiler writes for youfor await is section 2's while let, written by the compiler. For it to write the loop it has to know what to call, and two protocols tell it: AsyncSequence, with one requirement, makeAsyncIterator(), and AsyncIteratorProtocol, with next(). Here they are as the SDK declares them, shortened:
public protocol AsyncSequence<Element, Failure> {
associatedtype AsyncIterator : AsyncIteratorProtocol
associatedtype Element where Self.Element == Self.AsyncIterator.Element
associatedtype Failure = any Error where Self.Failure == Self.AsyncIterator.Failure
func makeAsyncIterator() -> Self.AsyncIterator
}
public protocol AsyncIteratorProtocol<Element, Failure> {
associatedtype Element
associatedtype Failure : Error = any Error
mutating func next() async throws -> Self.Element?
mutating func next(isolation actor: isolated (any Actor)?) async throws(Self.Failure) -> Self.Element?
}One struct can be both — its iterator is itself — and then the loop shrinks to one line:
/// The same countdown, usable with `for await`: `AsyncSequence` asks for `makeAsyncIterator()`,
/// `AsyncIteratorProtocol` for `next()`. One struct can be both.
struct Countdown: AsyncSequence, AsyncIteratorProtocol {
var left: Int
mutating func next() async -> Int? {
guard left > 0 else {
return nil
}
left -= 1
return left + 1
}
func makeAsyncIterator() -> Countdown {
self
}
}for await number in Countdown(left: 3) {
print(number)
}
print("Liftoff")It prints
3
2
1
LiftoffWhat the compiler does with that line is not a secret, and you can watch it. While it is still checking types, it rewrites every for-in loop, await or not, into a while loop, and -dump-ast prints the result under the loop as a desugared_loop. This is that tree for the loop above, trimmed to the nodes that matter:
(desugared_loop=brace_stmt
(pattern_binding_decl
(pattern_entry
(pattern_named type="Countdown" "$number$generator"
(init=call_expr type="Countdown"
(dot_syntax_call_expr type="() -> Countdown"
(declref_expr type="(Countdown) -> () -> Countdown" decl="Countdown.makeAsyncIterator()"
(while_stmt
(conditions=array
(pattern
(pattern_optional_some type="Int?"
(pattern_named type="Int" "$element"
(unsafe_expr type="Int?"
(call_expr type="Int?"
(dot_syntax_call_expr type="(isolated (any Actor)?) async -> Int?"
(declref_expr type="(inout Countdown) -> (isolated (any Actor)?) async -> Int?" decl="AsyncIteratorProtocol.next(isolation:)"
(argument_list
(argument inout
(inout_expr type="inout Countdown"
(declref_expr type="@lvalue Countdown" decl="Program.main().$number$generator"
(argument_list labels="isolation:"
(argument label="isolation"
(current_context_isolation_expr type="(any Actor)?"
(brace_stmt
(pattern_binding_decl
(pattern_entry
(init=declref_expr type="Int" decl="Program.main().$element"Read it from the top. A variable named $number$generator, set to Countdown(left: 3).makeAsyncIterator(). Then a while whose condition is let $element = await $number$generator.next(isolation: …) — pattern_optional_some is the let taking the value out of the optional — and whose body begins by binding your number to $element. Written as Swift you can type, it is this, and it prints the same:
// The loop above, as the compiler rewrites it — written as Swift you can type. The compiler's own names are
// `$number$generator` and `$element`: the `$` keeps them out of your code's way.
var generator = Countdown(left: 3).makeAsyncIterator()
while let element = await generator.next(isolation: #isolation) {
let number = element
print(number)
}
print("Liftoff")It prints
3
2
1
LiftoffSo the rewrite is section 2's loop with the protocols doing the naming. Three things follow. Why the protocols are required. The rewrite calls two methods it has never seen, and it finds them by looking up the conformance to AsyncSequence; without the conformance there is nothing to look up, and the loop fails before it is built. Lesson 40 tries it with the countdown of section 2, which conforms to nothing:
for await over a type with no conformance
for-in loop requires 'Countdown' to conform to 'AsyncSequence'
Where next runs. The rewrite passes #isolation — the actor the loop is on — to next(isolation:), which SE-0421 added. A nonisolated async function runs on the pool whoever calls it, so your own next() leaves the actor and comes back for every value, from a while let and from a for await alike; the default next(isolation:) runs on the loop's actor and then calls it. Implement next(isolation:) yourself and its isolated parameter keeps it on the caller's actor. The lesson measured both from a loop on the main actor:
A next() with no isolation, from a while let and then from a for await
next() on the main thread: false
next() on the main thread: false
next() on the main thread: false
next() on the main thread: falseThe same loop over a next(isolation:)
next(isolation:) on the main thread: true
next(isolation:) on the main thread: trueWhere the C++ is. The class that does the rewrite is DesugarForEachStmt, in the Swift compiler's type checker. Here it is, shortened to the lines that do what the tree shows: isAsync is whether you wrote await; sequenceProto is AsyncSequence for an await loop and Sequence for a plain one, so the same class rewrites both; buildMakeIteratorVar makes the $…$generator name; buildNextCall labels the argument isolation; and buildWhileStmt makes the while:
class DesugarForEachStmt {
ForEachStmt *stmt;
DeclContext *dc;
ASTContext &ctx;
bool isAsync;
…
DesugarForEachStmt(ForEachStmt *stmt)
: stmt(stmt), dc(stmt->getDeclContext()),
ctx(stmt->getDeclContext()->getASTContext()),
isAsync(stmt->getAwaitLoc().isValid()) {}
BraceStmt *operator()() {
…
sequenceProto =
isAsync ? ctx.getProtocol(KnownProtocolKind::AsyncSequence)
: (isBorrowing
? ctx.getProtocol(KnownProtocolKind::Iterable)
: ctx.getProtocol(KnownProtocolKind::Sequence));
seqConformanceRef = lookupConformance(seqType, sequenceProto);
…
buildMakeIteratorVar();
…
SmallVector<ASTNode, 2> stmts;
stmts.push_back(buildMakeIterator());
stmts.push_back(buildWhileStmt());
…
}
void buildMakeIteratorVar() {
std::string name;
{
if (auto np = dyn_cast_or_null<NamedPattern>(stmt->getPattern()))
name = "$" + np->getBoundName().str().str();
name += "$generator";
}
…
}
Expr *buildNextCall() {
…
FuncDecl *nextFn = TypeChecker::getForEachIteratorNextFunction(
dc, stmt->getForLoc(), isAsync);
TinyPtrVector<Identifier> labels;
if (nextFn && nextFn->getParameters()->size() == 1)
labels.push_back(ctx.Id_isolation);
…
}
WhileStmt *buildWhileStmt() {
Expr *nextCall = buildNextCall();
auto varName = isBorrowing ? "$span" : "$element";
…
auto *whileStmt = new (ctx)
WhileStmt(LabeledStmtInfo(), stmt->getForLoc(),
buildWhileCond(nextCallVar, nextCall), whileBody, true);
…
}
};After this rewrite, nothing further down the compiler knows that there was ever a for. The SIL it emits for main — its own intermediate code, one step before LLVM — shows the two calls, and nothing else of a loop:
// static Program.main()
// function_ref Countdown.makeAsyncIterator()
%12 = function_ref @$s7program9CountdownV17makeAsyncIteratorACyF : $@convention(method) (Countdown) -> Countdown
%13 = apply %12(%11) : $@convention(method) (Countdown) -> Countdown
// function_ref AsyncIteratorProtocol.next(isolation:)
%23 = function_ref @$sScIsE4next9isolation7ElementQzSgScA_pSgYi_tYa7FailureQzYKF : $@convention(method) @async <τ_0_0 where τ_0_0 : AsyncIteratorProtocol> (@sil_isolated @guaranteed Optional<any Actor>, @inout τ_0_0) -> (@out Optional<τ_0_0.Element>, @error_indirect τ_0_0.Failure)
try_apply %23<Countdown>(%16, %24, %21, %22) : $@convention(method) @async <τ_0_0 where τ_0_0 : AsyncIteratorProtocol> (@sil_isolated @guaranteed Optional<any Actor>, @inout τ_0_0) -> (@out Optional<τ_0_0.Element>, @error_indirect τ_0_0.Failure), normal bb2, error bb5makeAsyncIterator() once, then AsyncIteratorProtocol.next(isolation:), generic over the iterator, with the actor as its first argument and the iterator passed @inout so that each call can change it. What the protocols add is for everyone else: map, filter, prefix and the rest of AsyncSequence work on the countdown now, and so does any function that takes some AsyncSequence. Observations, the sequence of every change of an @Observable property, is one more such type: the guide @Observable, in full shows this same rewrite over it, and what its next does inside.
The rewrite is DesugarForEachStmt in lib/Sema/TypeCheckStmt.cpp of the Swift compiler, linked below at the commit this page quotes. SE-0298 is the proposal for AsyncSequence, AsyncIteratorProtocol and for await; SE-0421 is the one that gave next its isolation parameter.
Where this comes from: The Swift compiler's rewrite of every for-in loop, async or not: DesugarForEachStmt in lib/Sema/TypeCheckStmt.cpp · SE-0298: Async/Await: Sequences — AsyncSequence, AsyncIteratorProtocol and for await · SE-0421: Generalize effect polymorphism for AsyncSequence and AsyncIteratorProtocol — next(isolation:)
AsyncStreamSection 4's countdown waited for another thread to say so, and section 2's handed out values it already had. A stream is the two put together: values can arrive before anyone asks for them, so it keeps a buffer; and next can be asked before a value has arrived, so it keeps the continuation of whoever is waiting. yield and finish are the producer's side, on any thread; next is the consumer's. Here is one, in full:
import Foundation
import Synchronization
/// A stream of your own: a buffer for values nobody has asked for yet, and the continuation of whoever is waiting
/// for one. `yield` and `finish` are the producer's side; `next` is the consumer's.
final class MyStream<Element: Sendable>: Sendable {
private struct State {
var buffer: [Element] = []
var finished = false
var waiting: CheckedContinuation<Element?, Never>?
}
private let lock = Mutex(State())
/// A value goes to whoever is waiting, or into the buffer.
func yield(_ value: Element) {
let waiting = lock.withLock { state -> CheckedContinuation<Element?, Never>? in
if let waiting = state.waiting {
state.waiting = nil
return waiting
}
state.buffer.append(value)
return nil
}
waiting?.resume(returning: value)
}
/// The end: whoever is waiting gets `nil`, and so does every `next()` after the buffer is empty.
func finish() {
let waiting = lock.withLock { state -> CheckedContinuation<Element?, Never>? in
state.finished = true
let waiting = state.waiting
state.waiting = nil
return waiting
}
waiting?.resume(returning: nil)
}
/// A buffered value at once; `nil` after `finish`; otherwise park here until a value comes.
func next() async -> Element? {
await withCheckedContinuation { continuation in
lock.withLock { state in
if !state.buffer.isEmpty {
continuation.resume(returning: state.buffer.removeFirst())
} else if state.finished {
continuation.resume(returning: nil)
} else {
state.waiting = continuation
}
}
}
}
}A producer on another thread yields three values and finishes; the consumer is section 2's while let, unchanged:
let stream = MyStream<Int>()
Thread.detachNewThread {
for number in [3, 2, 1] {
Thread.sleep(forTimeInterval: 0.02)
stream.yield(number)
}
stream.finish()
}
while let number = await stream.next() {
print(number)
}
print("Liftoff")It prints
3
2
1
LiftoffThat is AsyncStream, in miniature. The real one is made the same way — Apple's page calls it "well-suited to adapt callback- or delegation-based APIs to participate with async-await" — and it is a sequence, so the loop can be for await:
let (stream, continuation) = AsyncStream.makeStream(of: Int.self)
Thread.detachNewThread {
for number in [3, 2, 1] {
Thread.sleep(forTimeInterval: 0.02)
continuation.yield(number)
}
continuation.finish()
}
for await number in stream {
print(number)
}
print("Liftoff")It prints
3
2
1
LiftoffmakeStream(of:) gives the sequence and its continuation — the producer's side, with yield and finish, which you may hand to a delegate method, a timer, or another thread. What the real one adds to MyStream is bookkeeping you would otherwise write yourself, and lesson 40 measures each piece of it. A buffering policy, for a producer faster than its consumer — .bufferingNewest(1) keeps only the last value:
Five values yielded, then one loop
[5]Values are kept until a loop takes them, so a producer may start before anyone is listening:
Two yields, then a loop
nobody was looping yet
Seattle
VancouverAn end on cancellation. When the task running the loop is cancelled, the stream ends by itself — onTermination runs, and next answers nil — which is the next section. And one reader: a second loop over the same stream shares the values rather than seeing them all, which is why each screen that needs a stream makes its own.
Task, and when the task is cancelledA loop like section 2's needs an async function to live in, so in an app it goes in a Task or, in a view, in .task, which starts when the view appears and cancels when the view goes away. Cancelling sets a flag; it stops nothing. A while let over a next() of your own runs on until next() answers nil, and a next() that never looks at the flag never does. Lesson 40 cancels one halfway:
A while let in a task cancelled after the second value
got 3 — cancelled: false
got 2 — cancelled: false
got 1 — cancelled: true
the loop endedThe third value came after the cancel. A next() that is meant to end with its task has to say so — here, by answering nil when its wait is cancelled:
The same, with a next() that notices
got 3 — cancelled: false
got 2 — cancelled: false
the loop endedAsyncStream does this for you — Apple: "Canceling an active iteration invokes the onTermination callback first, then resumes by yielding nil" — so a for await over a stream in a .task ends when the view does:
A loop over a stream, its task cancelled
terminated: cancelledOne more thing about a loop in a .task: the line after it runs only when the loop ends, and a loop over something that never finishes, such as a notification, ends only when the view goes away. The lesson shows it:
While the view is on the screen
the loop begins
became activeAfter it is removed
after the loopawait on an actor: the main actor, and one of your ownAn actor is an object that runs one job at a time, and the await you write to call into one is the same await as every other on this page: the function is cut, and the piece after the cut runs when the actor gets to it. The main actor is the one you have used most, and it is not a macro and not a special case. It is an actor like any you could write, with the @globalActor attribute and the one shared instance the GlobalActor protocol asks for:
@globalActor final public actor MainActor : GlobalActor {
public static let shared: MainActor
nonisolated final public var unownedExecutor: UnownedSerialExecutor { get }
public static var sharedUnownedExecutor: UnownedSerialExecutor { get }
nonisolated final public func enqueue(_ job: UnownedJob)
}
public protocol GlobalActor {
associatedtype ActorType : Actor
static var shared: Self.ActorType { get }
static var sharedUnownedExecutor: UnownedSerialExecutor { get }
}
public protocol Actor : AnyObject, Sendable {
nonisolated var unownedExecutor: UnownedSerialExecutor { get }
}So you can write one. Two lines make a global actor; @MyActor can then go on a type, a function or a closure, as @MainActor can, and everything marked with it takes turns on that one actor. This program marks one function with each and asks each where it runs, by the label of the dispatch queue under its thread:
import Foundation
/// A global actor of your own: the attribute, and the one shared instance the protocol asks for.
@globalActor
actor MyActor {
static let shared = MyActor()
}
/// The label of the dispatch queue this thread is serving: the main thread's, or one of the pool's.
func queueName() -> String {
String(cString: __dispatch_queue_get_label(nil))
}
@MainActor
func onTheMainActor() async {
print("@MainActor runs on", queueName())
}
@MyActor
func onMyActor() async {
print("@MyActor runs on", queueName(), "- and #isolation is MyActor.shared:", #isolation === MyActor.shared)
}
func onNoActor() async {
print("nonisolated async runs on", queueName())
}await onTheMainActor()
await onMyActor()
await onNoActor()It prints
@MainActor runs on com.apple.main-thread
@MyActor runs on com.apple.root.default-qos.cooperative - and #isolation is MyActor.shared: true
nonisolated async runs on com.apple.root.default-qos.cooperativeThe main actor runs on the main thread. Your actor runs on the pool, on whichever of its threads is free: an actor is not a thread, and not a queue either, but a promise that its jobs run one at a time, kept by the runtime on the shared threads. #isolation inside a @MyActor function is MyActor.shared, the instance the attribute names. And a function on no actor runs on the pool too, as section 5 said.
Now the one thing about actors that every await on this page has been preparing you for. An await inside an actor's method is a cut like any other: the piece before it ends, and the actor — which runs one job at a time — is free to run another job until the piece after it is ready. Shop has one bag. buy checks the stock, awaits the price, and sells:
/// Stands in for a request: it waits, then answers.
func askPrice() async -> Int {
try? await Task.sleep(for: .milliseconds(20))
return 5
}
/// One bag to sell. `buy` checks the stock, asks the price, and sells — with an `await` between the check and the sale.
actor Shop {
var bags = 1
var sold = 0
func buy() async {
guard bags > 0 else {
return
}
let price = await askPrice() // the actor is free while this waits
bags -= 1
sold += 1
print("sold a bag for", price)
}
}let shop = Shop()
async let first: Void = shop.buy()
async let second: Void = shop.buy()
_ = await (first, second)
let sold = await shop.sold
let bags = await shop.bags
print("sold:", sold, "- bags left:", bags)It prints
sold a bag for 5
sold a bag for 5
sold: 2 - bags left: -1Two bags sold, of one. Both buys saw a bag, because the second checked the stock while the first was waiting for the price, and the actor was free. Nothing was corrupted: the actor kept its promise, and every line ran one at a time. What it does not promise is that the world is as you left it on the far side of an await. The fix is to look again after the wait:
/// Stands in for a request: it waits, then answers.
func askPrice() async -> Int {
try? await Task.sleep(for: .milliseconds(20))
return 5
}
/// One bag to sell. `buy` checks the stock, asks the price, and sells — with an `await` between the check and the sale.
actor Shop {
var bags = 1
var sold = 0
func buy() async {
guard bags > 0 else {
return
}
let price = await askPrice() // the actor is free while this waits
guard bags > 0 else { // so look again: the world may have changed
return
}
bags -= 1
sold += 1
print("sold a bag for", price)
}
}It prints
sold a bag for 5
sold: 1 - bags left: 0This is reentrancy, and SE-0306 chose it on purpose, so that an actor waiting on something slow never holds up everything else that needs it. The rule to carry: what you checked before an await is a fact about before the await. Check it again, do the whole change after the wait, or keep the in-flight work somewhere so that a second caller joins it instead of starting it again — all three are in lesson 38.
The Actor protocol above has one requirement, unownedExecutor: the thing that runs the actor's jobs. The runtime provides one by default, on the pool. Give the actor one of your own and its jobs run there instead — here, on a serial dispatch queue:
import Foundation
/// The same global actor, now on a serial queue of its own: its jobs run there, one at a time — and may block it.
@globalActor
actor MyActor {
static let shared = MyActor()
private let queue = DispatchSerialQueue(label: "my.queue")
nonisolated var unownedExecutor: UnownedSerialExecutor {
queue.asUnownedSerialExecutor()
}
}
func queueName() -> String {
String(cString: __dispatch_queue_get_label(nil))
}
@MyActor
func onMyActor() async {
print("@MyActor now runs on", queueName())
}await onMyActor()It prints
@MyActor now runs on my.queueThe jobs now run on that queue — still one at a time, still with every await handing the actor back, exactly as before; a queue does not make an actor any less reentrant. What changes is which threads do the work, and that matters in one case: code that blocks. A Data(contentsOf:), a lock held for a while, a C library that sleeps — these hold their thread until they are done, and the pool's thread model counts on nobody doing that. Lesson 38 measures it: thirty saves that block for a tenth of a second each, on an actor on the pool and on an actor with a queue of its own, against other work that wants a turn every few milliseconds:
Thirty blocking saves, on the pool and on a queue of their own
on the pool: 30 saves took 312 ms; other work got 1 of the 31 turns it wanted
on its own queue: 30 saves took 105 ms; other work got 9 of the 10 turns it wantedOn the pool, other work got almost nothing: the pool has one thread per core and never adds one when a thread is blocked, so the blocked saves took the whole pool with them. On a queue of its own, the actor blocks its queue's thread and nobody else's. That is when to reach for an executor of your own: an actor whose work must block, and which must not take the pool down with it. When you do, the queue is still not a thread — a serial dispatch queue runs its blocks one at a time on whatever thread GCD gives it — and a queue that blocks is slow for its own callers; a usleep in a save is a usleep for everyone waiting on that actor.
And when not: every other time. A hop onto a queue actor goes through GCD, and a hop between two actors on the pool does not. The lesson measured the difference:
A hop between two actors, on the pool and on queues of their own
two plain actors: 144 ns a turn
two on queues of their own: 5367 ns a turnThirty-odd times slower, for an actor that does nothing but arithmetic. The same lesson measured a global actor against four plain ones, four pieces of work that could have run side by side, and found the one actor several times slower for the same reason that makes it right for the weather cache: one actor is one line, and everything in it waits for everything else:
The same work on four actors, and on one global actor
four actors of their own: 0.1 sec
four on one global actor: 0.4 sec| You want | Reach for | Because |
|---|---|---|
One copy of some state, shared by everyone, reached with await | Reach foran actor | Becauseone job at a time, on the pool, and a cheap hop |
| The same, named by an attribute you can put on types and functions | Reach fora @globalActor | Becauseone instance for the whole program, as @MainActor is |
| An actor whose work blocks its thread | Reach forthe same, with unownedExecutor on a DispatchSerialQueue | Becauseit blocks a queue of its own, never the pool |
| An actor whose jobs need one particular thread | Reach fora SerialExecutor of your own, with a thread of its own | Becausea queue is not a thread; SE-0392 shows the shape |
| To block on the pool | Reach fornothing | Becausethe pool never grows; wait with await instead |
That is the whole arrangement, and it comes down to the first section. An await ends a function and makes a new one; a task is frames and an address, not a thread; a continuation is the way back in; for await and AsyncStream are the loop and the wait with names; and an actor is a promise that its jobs take turns, kept by running each piece when its turn comes. Every piece of it was compiled and run for this page, and every number on it is what the run printed.