01 · INTRODUCTION
What Actually Happens When a Swift Task Suspends?
When a Swift Task suspends, the Task temporarily stops executing without needing to keep a thread blocked while it waits to continue. Its continuation can later become eligible to execute again when the asynchronous operation it is waiting for allows it to proceed.
That definition is technically useful, but it does not yet explain why suspension matters.
To really understand suspension, we need to think about concurrency differently.
Concurrency is not simply about making one operation finish as quickly as possible. It is about allowing multiple pieces of work to make progress together without one unnecessarily preventing the others from progressing.
We already understand this idea at the operating-system level. We would never want iOS to let one running application monopolise the processor until that application had finished everything it wanted to do. The operating system schedules work so that many running applications can continue making progress over the same period of time.
Swift Concurrency asks us to begin thinking about the work inside our own applications with a similar mindset.
💡 The Mental Model
Concurrency is about progress.
Instead of asking how much work one feature can execute right now, begin asking how all of the important work in your application can continue making progress together.
02 · TUTORIAL
Imagine Processing 1,000 Items
Suppose our application has an array containing 1,000 values and we need to perform some computation on every item.
CODE EXAMPLE
for item in items {
process(item)
}
There is nothing inherently wrong with this code.
The loop is continuously making progress. It processes the first item, then the second, then the third, and continues until all 1,000 items have been processed.
If the computation takes 200 milliseconds, the processor may be doing useful work throughout those entire 200 milliseconds.
But there is another question that matters enormously to an iOS developer:
Does all 1,000-item work actually need to finish right now before anything else is allowed to make progress?
If this synchronous computation occupies the main execution context for 200 milliseconds, the calculation may be progressing beautifully while the application experience is terrible.
The interface may stop responding smoothly.
Animations can miss frames.
Scrolling can become jerky.
The user experiences a frozen application even though the processor itself is extremely busy.
03 · TUTORIAL
Fast Code Can Still Produce a Slow Application
This distinction is extremely useful.
Imagine our 1,000-item computation takes only:
CODE EXAMPLE
0.2 seconds
Two tenths of a second sounds fast.
As a standalone benchmark, we might even be pleased with it.
But 200 milliseconds is a very long period of time when we are trying to maintain a responsive graphical interface.
At 60 frames per second, a new frame opportunity occurs roughly every:
CODE EXAMPLE
16.7 milliseconds
Two hundred milliseconds therefore spans roughly twelve 60 Hz frame intervals.
If our main execution context is monopolised throughout that period, the user does not think:
CODE EXAMPLE
Excellent.
That algorithm completed
in only 0.2 seconds.
They see the interface stop moving.
This gives us a much more useful definition of performance.
💡 Important Idea
The goal is not always to finish one piece of work in the shortest possible wall-clock time.
For interactive software, it is often more important that the application as a whole remains responsive while useful work continues to make progress.
04 · TUTORIAL
Think in Frames of Animation
iOS applications are visual, interactive programs.
Our users scroll lists, tap buttons, drag controls, watch transitions and expect animations to remain smooth while the application is doing other work.
This means it can be useful to think about our application over time.
EXECUTION DIAGRAM
TIME ─────────────────────────────────────────────▶
Frame Frame Frame Frame
│ │ │ │
▼ ▼ ▼ ▼
UI work UI work UI work UI work
██ ██ ██ ██
Other work
███ ███ ███ ███
This diagram is not intended to describe the exact internal scheduling algorithm used by Swift or iOS.
It is a mental model for responsiveness.
We want our application to continue presenting frames, responding to the user and progressing other important work rather than allowing one long-running operation to monopolise the execution context required by everything else.
05 · TUTORIAL
Perhaps We Do Not Need to Finish Everything Right Now
Return to our 1,000 items.
Our first mental model was:
EXECUTION DIAGRAM
Process all 1,000 items
████████████████████████████████████████
DONE
The operation begins and attempts to finish all of its synchronous work before returning.
But imagine that our application did not actually require all 1,000 items to be processed during one uninterrupted period of execution.
Conceptually, the work could instead make progress in smaller units:
EXECUTION DIAGRAM
Items 1–100
████
Other eligible work
██
Items 101–200
████
Other eligible work
██
Items 201–300
████
Other eligible work
██
The total computation may still require roughly the same amount of useful CPU work.
It might even finish slightly later in wall-clock time.
But the application can remain responsive because the computation is no longer imagined as the only work that deserves to make progress.
This is the beginning of the cooperative mental model.
06 · TUTORIAL
We Already Use This Idea Across Running Applications
This should feel familiar because we have spent this entire article series building the same idea from the operating system upward.
Imagine an operating system without concurrency.
EXECUTION DIAGRAM
Photos
████████████████████████
FINISH EVERYTHING
Messages
████████████████
FINISH EVERYTHING
Safari
███████████████
That would be an awful multitasking system.
We do not want Photos to finish everything it could possibly calculate before Messages receives an opportunity to execute.
Instead, processor resources are shared over time.
EXECUTION DIAGRAM
TIME ─────────────────────────────────────────────▶
Photos
████ ████ ████
Messages
████ ████
Safari
████ ████
Each application can make progress over the same period of time.
That is concurrency.
07 · TUTORIAL
Now Bring That Mental Model Inside Our App
Our application itself contains many pieces of work.
CODE EXAMPLE
Render UI
Load Launches
Decode JSON
Save Favourites
Load Images
Respond to User Input
Refresh Account
Update Animations
Why should one of those features assume that once it begins executing, it deserves to finish absolutely everything before other important work can progress?
This is where our mental model begins to change.
Instead of thinking:
CODE EXAMPLE
I have execution now.
Finish everything.
we begin thinking:
CODE EXAMPLE
I have work to perform.
How can that work make progress
while the rest of the application
also continues making progress?
That is a much more useful way to approach modern concurrent application design.
08 · TUTORIAL
Could We Chop Our Work Into Smaller Pieces?
Conceptually, yes.
If a large computation does not need to happen atomically as one uninterrupted operation, we can sometimes design it as smaller units of work and provide opportunities for other eligible work to execute between those units.
Instead of:
EXECUTION DIAGRAM
Task A
████████████████████████████████████████
we can aim for a system that behaves more like:
EXECUTION DIAGRAM
Task A
████ ████ ████
Task B
████ ████
Task C
████ ████
Now several pieces of logical application work are making progress over the same period.
This is the same powerful idea that made operating-system multitasking possible, applied at a different abstraction layer.
09 · TUTORIAL
But Swift Does Not Automatically Chop Up Your Code
This is where we must be precise.
Swift Concurrency does not take arbitrary synchronous code such as:
CODE EXAMPLE
for item in items {
process(item)
}
and secretly transform it into:
CODE EXAMPLE
Process 100 items
Stop
Run another Task
Process another 100 items
Stop
Run another Task
Continue later
That is not what cooperative Swift concurrency means.
If our synchronous loop takes 200 milliseconds, it can continue executing synchronously for those 200 milliseconds.
Swift does not automatically decide that we have used enough execution time and preempt the Task at some convenient Swift source-code line.
💡 This Is Important
Swift Concurrency does not automatically interrupt long-running synchronous code and divide it into tiny concurrent chunks.
Synchronous work remains synchronous work. We have to architect our application so that long-running work does not unnecessarily monopolise execution resources needed by other important work.
10 · TUTORIAL
This Is Where the Word Cooperative Matters
The system is called cooperative for a reason.
Our code participates in the execution model.
Swift provides mechanisms through which asynchronous work can suspend and later continue. It provides Tasks, executors, structured concurrency and actor isolation.
But it does not magically turn every synchronous algorithm into perfectly interleaved concurrent work.
We still need to understand what our code is doing.
We still need to identify expensive synchronous work.
We still need to decide where that work belongs.
And when appropriate, we may need to structure large computations into meaningful units that allow other work to make progress.
11 · TUTORIAL
Now We Can Understand Suspension
With this mental model in place, suspension becomes much easier to understand.
Imagine our Task begins an asynchronous network operation:
CODE EXAMPLE
let launches = try await api.loadLaunches()
Our Task begins executing.
Eventually it reaches the asynchronous call.
The result may not yet be available.
Perhaps we have sent a network request and the response will arrive later.
At this point, keeping the current Task continuously executing cannot magically make the remote server respond sooner.
More importantly, we do not need to keep a system thread synchronously blocked simply to represent the fact that this Task has unfinished work.
The Task can suspend.
12 · TUTORIAL
A Suspended Task Is Still an Unfinished Task
Consider:
CODE EXAMPLE
func loadLaunch() async throws -> Launch {
let data = try await api.loadLaunchData()
let launch = try decoder.decode(
Launch.self,
from: data
)
return launch
}
If suspension occurs at the await, the function has not finished.
The Task has not disappeared.
The code after the asynchronous call still needs to execute.
EXECUTION DIAGRAM
Begin loadLaunch()
│
▼
Start loadLaunchData()
│
▼
await
│
▼
SUSPEND
⋮
⋮
Data becomes available
│
▼
Continuation becomes eligible
│
▼
Decode Launch
│
▼
Return Launch
The Task's lifetime continues across that period of suspension.
13 · TUTORIAL
Suspension Is Not the Same as Blocking
This is one of the most useful distinctions in Swift Concurrency.
Imagine synchronous waiting.
EXECUTION DIAGRAM
THREAD
Begin work
│
▼
WAIT WAIT WAIT WAIT WAIT WAIT WAIT
██████████████████████████████████
│
▼
Continue
The thread is tied up in that synchronous wait.
Now compare that with Task suspension:
EXECUTION DIAGRAM
TASK A
████████
│
▼
SUSPEND
⋮
⋮
│
▼
CONTINUE
The Task is unfinished, but the system thread that happened to be executing its current work does not need to remain permanently attached to it while it is suspended.
That execution resource can potentially be used for other eligible work.
14 · TUTORIAL
The Task and the Thread Are Different Things
This is why the distinction between Tasks and threads is so important.
A Task represents logical asynchronous work.
A thread is an operating-system execution context through which instructions can ultimately reach a processor core.
The relationship is not:
CODE EXAMPLE
Task A = Thread 1
Task B = Thread 2
Task C = Thread 3
A better mental model is:
EXECUTION DIAGRAM
Task A ──┐
Task B ──┤
Task C ──┤
Task D ──┤
Task E ──┘
│
▼
Eligible Jobs
│
▼
Executors
│
▼
System Threads
│
▼
OS Scheduler
│
▼
CPU Cores
A Task can therefore have a much longer lifetime than any one uninterrupted period of thread execution.
15 · TUTORIAL
await Marks a Potential Suspension Point
Consider:
CODE EXAMPLE
let launches = try await api.loadLaunches()
The await is important because it tells us that execution may suspend while performing this asynchronous call.
But we need to be careful with the language we use.
await does not mean:
CODE EXAMPLE
Create another thread.
It does not mean:
CODE EXAMPLE
Move this code to the background.
And it does not mean:
CODE EXAMPLE
The Task definitely suspends here.
It marks a point at which suspension is possible.
💡 await
await identifies a potential suspension point in an asynchronous function.
It should make us aware that when execution continues afterward, time may have passed and other permitted concurrent work may have progressed.
16 · TUTORIAL
Think of Suspension as a Save Point
A useful mental model is a save point in a game.
Imagine our Rocket Launch Task has progressed through several stages:
CODE EXAMPLE
ROCKET LAUNCH TASK
✓ Build Request
✓ Start Download
🚀
[ SAVE POINT ]
○ Receive Data
○ Decode Launches
○ Update Model
○ Finish
We do not need to keep the character actively moving simply because the game has not finished.
We can preserve enough information to continue later.
A suspended Swift Task follows a loosely similar conceptual idea.
Its asynchronous lifetime continues, but it does not need to continuously occupy a thread while waiting to become eligible to continue.
17 · TUTORIAL
The Thread Can Now Perform Other Work
Suppose Task A was executing using a system thread.
EXECUTION DIAGRAM
Task A
│
▼
Thread
│
▼
CPU Core
Task A reaches a suspension point and actually suspends.
We no longer need:
EXECUTION DIAGRAM
Task A
│
▼
Thread
│
▼
WAITING
│
▼
WAITING
│
▼
WAITING
Instead, Task A can remain suspended while the execution resource becomes available for other eligible work.
EXECUTION DIAGRAM
Task A
SUSPENDED
Task B
│
▼
Thread
│
▼
CPU Core
This is how large numbers of asynchronous operations can exist without requiring one permanently dedicated thread for every unfinished operation.
18 · TUTORIAL
Now Return to Our Frames
This brings us back to the application experience.
Imagine several pieces of asynchronous work:
EXECUTION DIAGRAM
TIME ─────────────────────────────────────────────▶
Task A
████ SUSPENDED ████
Task B
████ ████
Task C
████ ████
UI work
██ ██ ██ ██ ██
Again, this is a conceptual diagram rather than an exact representation of Swift's scheduler.
But it shows the design goal beautifully.
Our application contains many things that need to make progress.
Some are executing.
Some are waiting.
Some are suspended.
Some become eligible again.
The goal is not to let one piece of work unnecessarily dominate the entire application.
19 · TUTORIAL
What About CPU-Heavy Work?
This is where our original 1,000-item loop becomes important again.
CODE EXAMPLE
for item in items {
process(item)
}
This loop does not suspend simply because it takes a long time.
It may continue executing synchronously until all 1,000 items have been processed.
If that work belongs somewhere that blocks important progress elsewhere in the application, we have an architectural problem to solve.
Depending on the work, one possible design is to divide a large computation into meaningful batches and deliberately provide opportunities for other eligible work to run.
Conceptually:
EXECUTION DIAGRAM
Batch 1
████
Allow other eligible work to progress
██
Batch 2
████
Allow other eligible work to progress
██
Batch 3
████
Swift provides mechanisms such as Task.yield() that can be useful in particular cooperative workloads.
But this should not be turned into another rule to memorise such as “put Task.yield() inside every loop.”
The important lesson is architectural.
Break long-running work into meaningful units when the entire operation does not need to execute atomically, and provide opportunities for other eligible work to make progress when appropriate.
20 · TUTORIAL
Async Does Not Automatically Make CPU Work Cooperative
Consider:
CODE EXAMPLE
@MainActor
func calculateEverything() async {
for item in items {
performExpensiveCalculation(item)
}
}
There is an async keyword in the function declaration.
That does not mean Swift automatically pauses the loop every few milliseconds.
It does not mean the computation automatically moves away from the MainActor.
It does not mean the UI is guaranteed to remain responsive.
If that synchronous work occupies the MainActor's execution context for too long, our interface can still suffer.
Async gives a function the ability to participate in asynchronous execution and suspension. It does not transform every synchronous instruction inside that function into automatically interleaved work.
21 · TUTORIAL
This Is Why Async Does Not Mean Background
We can now understand another phrase that developers frequently memorise:
CODE EXAMPLE
async does not mean background
Consider:
CODE EXAMPLE
@MainActor
final class LaunchModel {
var launches: [Launch] = []
func refresh() async throws {
launches = try await api.loadLaunches()
}
}
The method is MainActor isolated.
It is also asynchronous.
Those facts are not contradictory.
The function can begin executing in the MainActor's isolation domain, reach the asynchronous operation and potentially suspend.
While suspended, it does not need to synchronously block the main thread merely because its Task has not finished.
Later, its continuation can become eligible to execute again while respecting the MainActor's isolation requirements.
22 · TUTORIAL
This Is Exactly What We Want for UI Code
Think again about our animation frames.
We do not want:
EXECUTION DIAGRAM
MAIN EXECUTION
Start refresh()
████████████████████████████████████████
Network response finally arrives
Update UI
We want the asynchronous lifetime of the operation to exist without requiring the main execution context to remain blocked throughout the wait.
EXECUTION DIAGRAM
Start refresh()
████
│
▼
await
│
▼
SUSPEND
UI continues
██ ██ ██ ██ ██
Network result becomes available
│
▼
Continue Task
███
Update model
This is why suspension is so valuable for interactive software.
23 · TUTORIAL
Suspension Also Changes Our Assumptions About State
There is another consequence that becomes very important when we begin studying actors.
Consider:
CODE EXAMPLE
actor LaunchStore {
var launches: [Launch] = []
func refresh() async throws {
let newLaunches = try await api.loadLaunches()
launches = newLaunches
}
}
The actor method can reach the await and suspend.
While that operation is suspended, other eligible work for the actor may execute.
That means an actor method should not always assume that every piece of actor state remains exactly as it was before an await.
EXECUTION DIAGRAM
Actor Method
Read State
│
▼
await
│
▼
SUSPEND
│
│
│ Other eligible actor work
│ may execute
│
▼
RESUME
│
▼
Continue
This is known as actor reentrancy.
We will study it properly later, but suspension is the concept that makes it possible to understand.
24 · TUTORIAL
Swift Concurrency Adds Another Layer of Concurrency
We can now connect everything we have learned.
The operating system already provides concurrency between threads.
EXECUTION DIAGRAM
Thread A
████ ████
Thread B
████ ████
Swift Concurrency introduces a higher-level model for coordinating logical application work.
EXECUTION DIAGRAM
Task A
████ SUSPEND ████
Task B
████ ████
Task C
████ ████
│
▼
EXECUTORS
│
▼
SYSTEM THREADS
│
▼
OS SCHEDULER
│
▼
CPU CORES
The lower-level machinery still exists.
Processes still exist.
Threads still exist.
The OS scheduler still exists.
CPU cores still execute machine instructions.
Swift Concurrency gives us another layer through which the logical work inside our application can be structured and coordinated.
25 · TUTORIAL
The Big Change Is How We Think About Progress
Perhaps the most useful thing to take away from suspension is not the implementation detail.
It is the change in how we think about our application.
Our old instinct might be:
CODE EXAMPLE
I started this work.
I should finish all of it
as quickly as possible.
Our concurrent instinct should increasingly become:
CODE EXAMPLE
This work needs to make progress.
What other work also needs
to make progress?
How can the system remain responsive
while all of it progresses?
That is a much more useful question for an iOS developer.
26 · TUTORIAL
What to Remember
💡 What to Remember
Concurrency is about allowing multiple pieces of work to make progress over the same period of time.
Finishing one operation in the shortest possible time is not always the most important measure of application performance.
A 200 millisecond synchronous computation may sound fast, but if it monopolises the main execution context it can span roughly twelve 60 Hz frame intervals and produce a visibly unresponsive interface.
Large computations do not always need to execute as one uninterrupted block of work. When appropriate, work can be architected into meaningful units that allow other eligible work to progress.
Swift Concurrency does not automatically chop arbitrary synchronous code into tiny pieces.
A long synchronous loop remains synchronous unless the architecture introduces appropriate concurrency or suspension opportunities.
await marks a potential suspension point. It does not mean “create a thread,” “move to the background,” or “definitely suspend.”
When a Swift Task actually suspends, the Task remains unfinished but temporarily stops executing.
The Task does not need to keep the system thread that was executing it permanently blocked while it waits.
When the awaited operation allows progress to continue, the Task's continuation can later become eligible for execution again through the appropriate executor.
A Task does not permanently own one particular system thread.
Async code is not automatically background code, and CPU-heavy synchronous work does not become cooperative merely because it exists inside an async function.
The modern mental model is:
EXECUTION DIAGRAM
Many pieces of application work
↓
Make progress together
↓
Suspend when appropriate
↓
Allow other eligible work to run
↓
Continue when ready
↓
Keep the application responsive
27 · TUTORIAL
Your Next Move
We now have a much stronger picture of what Swift means by cooperative concurrency.
Our goal is not simply to create more threads.
Our goal is not to move everything away from the main thread.
And our goal is not to execute as much code as physically possible every time one feature gets an opportunity to run.
Our application contains many pieces of work that all need opportunities to make progress.
Swift Tasks give us a way to describe that asynchronous work.
Suspension allows a Task to remain unfinished without requiring continuous thread occupation.
And when a suspended Task becomes ready to continue, something has to decide how that eligible Swift work should execute.
That brings us to one of the most important pieces of the Swift Concurrency Runtime:
the executor.
What exactly is an executor?
Is it a thread?
Is it a queue?
Does every Task have one?
Does every actor have one?
And how does an executor eventually connect our Swift code to the system threads that are scheduled onto the processor?
That is the next article:
What Is an Executor in Swift Concurrency?
