01 · INTRODUCTION
What Is an Executor in Swift Concurrency?
An executor is the Swift Concurrency service that accepts eligible jobs and arranges for them to execute.
That sounds simple, but the executor is one of the most important pieces of the Swift Concurrency Runtime because it sits directly between the logical work we describe with Tasks and the lower-level threads that eventually execute machine instructions.
We have already established that a Swift Task is not a thread. A Task can begin, suspend, remain unfinished for a period of time, and later continue without permanently owning one system thread for its entire lifetime.
That creates an obvious problem.
If Tasks do not own threads, then when some Task work becomes ready to execute, who decides how that eligible work gets access to an execution resource?
That is where executors enter the architecture.
💡 The Answer
An executor is a service that executes Swift concurrency jobs.
Tasks describe asynchronous work. Runnable portions of that work become jobs. Executors arrange the execution of those jobs using underlying system execution resources.
02 · TUTORIAL
Start With the Model We Already Know
We have gradually built this execution model throughout the previous articles:
EXECUTION DIAGRAM
Swift Task
│
▼
Runnable Job
│
▼
Executor
│
▼
System Thread
│
▼
OS Scheduler
│
▼
CPU Core
│
▼
Machine Instructions
The executor lives in the middle of this model.
Above it is the Swift Concurrency world of Tasks, asynchronous functions, actors and isolation.
Below it is the operating-system world of threads, scheduling and processors.
This is why an executor can initially be difficult to understand. It is neither the high-level Task we wrote nor the physical execution resource that ultimately runs instructions.
It is part of the machinery that connects the two.
03 · TUTORIAL
A Task Is the Whole Asynchronous Operation
Imagine this Task:
CODE EXAMPLE
Task {
let launches = try await api.loadLaunches()
await model.update(launches)
}
The Task represents the asynchronous operation across its entire lifetime.
That lifetime might look like this:
EXECUTION DIAGRAM
TASK
Begin
│
▼
Run some work
│
▼
await loadLaunches()
│
▼
SUSPEND
│
│
│ waiting
│
▼
Become eligible again
│
▼
Continue work
│
▼
Finish
The Task is therefore larger than one uninterrupted period of processor execution.
It can contain several separate periods during which Swift code is actually runnable.
04 · TUTORIAL
A Job Is Runnable Work
This is why it helps to distinguish the Task from a job.
Think of a job as a schedulable unit of synchronous Swift concurrency work that is ready to execute.
Our asynchronous Task may therefore produce separate runnable regions over its lifetime.
EXECUTION DIAGRAM
TASK
JOB A
████████
│
▼
await
│
▼
SUSPENDED
⋮
⋮
ready again
│
▼
JOB B
████████
│
▼
finish
The Task represents the complete asynchronous operation.
The jobs are the pieces of that operation that can currently execute.
And once a job becomes eligible to execute, it needs somewhere to go.
05 · TUTORIAL
The Executor Accepts Eligible Jobs
This is the role of the executor.
Conceptually:
EXECUTION DIAGRAM
JOB A ───────┐
│
JOB B ───────┼────▶ EXECUTOR
│
JOB C ───────┘
The executor is responsible for arranging execution of the jobs that belong to its execution domain.
Eventually those jobs must execute using underlying system threads.
EXECUTION DIAGRAM
Eligible Jobs
│
▼
Executor
│
▼
System Execution Resources
│
▼
System Threads
│
▼
CPU Cores
This is the bridge we were missing when we first learned that Tasks do not own threads.
06 · TUTORIAL
An Executor Is Not a Thread
This distinction should be made immediately and repeatedly until it becomes natural.
An executor is not a system thread.
A thread is an operating-system execution context.
An executor is part of Swift's concurrency model and is responsible for executing jobs.
So this:
CODE EXAMPLE
EXECUTOR = THREAD
is the wrong mental model.
A better model is:
EXECUTION DIAGRAM
EXECUTOR
│
│ arranges eligible jobs
│ for execution
▼
SYSTEM EXECUTION RESOURCES
│
▼
THREADS
│
▼
CPU
💡 Important Idea
An executor describes where Swift concurrency jobs are allowed or expected to execute within Swift's concurrency model. It is not itself the operating-system thread that runs the machine instructions.
07 · TUTORIAL
An Executor Is Not a CPU Core Either
For the same reason, an executor is not a processor core.
The processor is hardware.
The executor is software machinery inside the concurrency system.
The relationship is layered:
EXECUTION DIAGRAM
Swift Job
│
▼
Executor
│
▼
System Thread
│
▼
OS Scheduler
│
▼
CPU Core
Once this hierarchy is clear, many confusing concurrency explanations become much easier to decode.
08 · TUTORIAL
Is an Executor a Queue?
This comparison is more tempting because executors and queues both seem to organise work.
But we should still avoid treating the words as synonyms.
Grand Central Dispatch gives us dispatch queues:
CODE EXAMPLE
DispatchQueue.main.async {
updateUI()
}
Swift Concurrency gives us executors as part of its Task and isolation model.
Both systems can organise executable work, but they belong to different programming abstractions and have different contracts.
So rather than saying:
CODE EXAMPLE
An executor is basically a queue.
it is better to learn the real definition:
An executor is a service that executes jobs.
That definition remains useful even as Swift's executor APIs become more sophisticated.
09 · TUTORIAL
Why Do We Need Executors at All?
Imagine a concurrency runtime with Tasks but no executor concept.
We create:
CODE EXAMPLE
Task A
Task B
Task C
Task D
Task E
Task A reaches a suspension point.
Task B becomes runnable.
Task C belongs to an actor.
Task D needs MainActor isolation.
Task E is nonisolated concurrent work.
Something now needs to understand that these jobs do not all have identical execution requirements.
For example:
CODE EXAMPLE
JOB A
general concurrent work
JOB B
must respect Actor A isolation
JOB C
must respect MainActor isolation
The executor system provides the machinery through which those execution requirements can be expressed and enforced.
10 · TUTORIAL
Actors Make Executors Easier to Understand
Consider an actor:
CODE EXAMPLE
actor LaunchStore {
private var launches: [Launch] = []
func save(_ launch: Launch) {
launches.append(launch)
}
}
The actor protects its isolated mutable state.
That means Swift cannot allow two independently executing actor-isolated jobs to mutate that state simultaneously.
Conceptually, imagine two Tasks both want to call:
CODE EXAMPLE
await launchStore.save(launch)
We do not want:
EXECUTION DIAGRAM
Actor Job A
████████
Actor Job B
████████
both executing actor-isolated
state mutation simultaneously
Instead, the actor's serial executor ensures that actor-isolated jobs execute one at a time.
EXECUTION DIAGRAM
ACTOR EXECUTION
Job A
████████
│
▼
Job B
████████
│
▼
Job C
████████
That is one of the key mechanisms that makes actor isolation useful.
11 · TUTORIAL
Actors Use Serial Executors
This gives us an important executor category.
An actor is associated with a serial executor.
Serial means that jobs isolated to that actor are not executed simultaneously with one another.
Conceptually:
EXECUTION DIAGRAM
Actor
Job A ───────┐
Job B ───────┼────▶ Serial Executor
Job C ───────┘
│
▼
execute one job
at a time
This does not mean the actor owns one permanent thread.
That distinction matters enormously.
The serial ordering requirement belongs to the executor and actor-isolation model.
It does not require:
CODE EXAMPLE
Actor A = Thread 7 forever
The actor's work can be serialised without permanently dedicating one operating-system thread to that actor.
12 · TUTORIAL
Serial Execution Does Not Mean One Permanent Thread
This is worth seeing visually.
The wrong mental model is:
EXECUTION DIAGRAM
ACTOR
│
▼
THREAD 4
│
▼
Actor always lives
on Thread 4 forever
The useful mental model is:
EXECUTION DIAGRAM
ACTOR JOBS
Job A
Job B
Job C
│
▼
SERIAL EXECUTOR
│
▼
Jobs execute
one at a time
│
▼
Underlying execution resources
The actor's safety comes from its isolation and serial execution rules, not from owning a named thread.
13 · TUTORIAL
This Is a Huge Change From Thread-Based Thinking
Traditional concurrency often encouraged questions such as:
CODE EXAMPLE
Which thread owns this state?
Which queue protects this property?
Which lock should I use?
Swift actors allow us to ask a more architectural question:
CODE EXAMPLE
Which isolation domain owns this state?
Then the executor system helps Swift enforce the execution rules associated with that domain.
This is much more powerful than simply attaching a variable to a thread.
14 · TUTORIAL
Now Consider the MainActor
The most familiar global actor for an iOS developer is MainActor.
CODE EXAMPLE
@MainActor
final class LaunchModel {
var launches: [Launch] = []
}
The type's isolated state belongs to the MainActor.
Work requiring that isolation must execute through the MainActor's executor.
On Apple platforms, this execution domain is associated with main-thread execution.
A simplified model is:
EXECUTION DIAGRAM
MainActor-isolated Job
│
▼
MainActor Executor
│
▼
Main-thread execution
│
▼
OS Scheduler
│
▼
CPU Core
This is why MainActor and the main thread are closely related while still not being the same concept.
15 · TUTORIAL
MainActor Is an Isolation Domain, Not a Thread Alias
This distinction becomes easier now that we understand executors.
The main thread belongs to the operating-system layer.
MainActor belongs to Swift's concurrency and isolation model.
Its executor provides the bridge.
EXECUTION DIAGRAM
SWIFT
@MainActor
│
▼
Isolation requirement
│
▼
MainActor Executor
│
▼
Main-thread execution
OPERATING SYSTEM
Main Thread
│
▼
OS Scheduling
│
▼
CPU
This is a much richer model than simply saying:
CODE EXAMPLE
@MainActor means main thread.
That shorthand may occasionally help, but it hides the isolation architecture Swift actually gives us.
16 · TUTORIAL
What About Work That Does Not Belong to an Actor?
Not every asynchronous function is actor isolated.
Swift also has a default global concurrent executor for work that does not have a stricter executor requirement.
Conceptually:
EXECUTION DIAGRAM
Nonisolated Concurrent Work
Job A ──────────┐
Job B ──────────┤
Job C ──────────┼──▶ Global Concurrent Executor
Job D ──────────┤
Job E ──────────┘
│
▼
Shared execution resources
This work does not require the one-at-a-time isolation guarantee of one particular actor.
Eligible jobs can therefore participate in concurrent execution.
17 · TUTORIAL
Serial and Concurrent Are Different Requirements
Now we can compare two executor environments.
EXECUTION DIAGRAM
ACTOR SERIAL EXECUTOR
Job A
████████
Job B
████████
Job C
████████
GLOBAL CONCURRENT EXECUTION
Job X
████████████
Job Y
████████████
Job Z
████████████
The first model protects an actor's isolated state by ensuring actor-isolated jobs execute serially.
The second supports work that can make progress concurrently.
This is why executors are deeply connected to isolation.
18 · TUTORIAL
An Executor Does Not Necessarily Mean Immediate Execution
Suppose several jobs become eligible for one actor.
CODE EXAMPLE
Job A
Job B
Job C
Job D
The actor's executor cannot execute all four actor-isolated jobs simultaneously because that would violate the actor's serial isolation model.
Instead, they wait for opportunities to execute through that executor.
EXECUTION DIAGRAM
Eligible Actor Jobs
A
B
C
D
│
▼
Serial Executor
│
├── execute A
│
├── execute B
│
├── execute C
│
└── execute D
The exact ordering of independently submitted work should not be guessed unless Swift's concurrency semantics provide an ordering guarantee for the particular situation.
The important executor guarantee here is serial execution of actor-isolated jobs, not that developers can infer arbitrary global ordering from source-code appearance.
19 · TUTORIAL
Now Add Suspension
This is where executors and suspension connect beautifully.
Imagine an actor-isolated method:
CODE EXAMPLE
actor LaunchStore {
var launches: [Launch] = []
func refresh() async throws {
let newLaunches = try await api.loadLaunches()
launches = newLaunches
}
}
The method begins executing actor-isolated work.
EXECUTION DIAGRAM
Actor Job A
████████
│
▼
await
If the Task suspends, Job A is no longer continuously occupying the actor's executor while waiting.
That creates an opportunity for other eligible actor work to execute.
EXECUTION DIAGRAM
Actor Job A
████████
│
▼
SUSPEND
Actor Job B
████████
Actor Job C
████████
Later...
Actor Job A continuation
████████
This is actor reentrancy beginning to appear in our model.
20 · TUTORIAL
The Actor Is Serial, but the Task Can Still Suspend
This can initially sound contradictory.
If an actor executes jobs serially, how can another actor job execute while the first actor method has not finished?
Because the whole asynchronous method is not one permanently executing job.
The Task can suspend.
Its current job finishes its period of synchronous execution.
Other eligible actor jobs can then execute through the serial executor.
Later, the original Task's continuation can become another eligible actor-isolated job.
Conceptually:
EXECUTION DIAGRAM
TASK A
Job A1
██████
│
▼
suspend
⋮
⋮
Job A2
██████
ACTOR EXECUTOR
Job A1
██████
Job B
██████
Job C
██████
Job A2
██████
The actor never had two isolated jobs executing simultaneously.
But other actor work made progress while Task A was suspended.
21 · TUTORIAL
This Is Why State Can Change Across await
Now actor reentrancy stops sounding mysterious.
Consider:
CODE EXAMPLE
actor LaunchStore {
var selectedID: Int?
func select(_ id: Int) async throws {
selectedID = id
let details = try await loadDetails(id)
save(details)
}
}
Before the await:
CODE EXAMPLE
selectedID = id
Task A is executing actor-isolated work.
Then it suspends.
Another actor-isolated job can execute through the same serial executor and potentially change selectedID.
When Task A later continues, the actor is still data-race safe, but the logical state may no longer match assumptions made before suspension.
The executor has protected simultaneous actor-isolated access.
It has not frozen the actor's state for the entire lifetime of every asynchronous method call.
22 · TUTORIAL
Executors Protect Execution Rules, Not Business Logic
This is an important distinction.
The executor can enforce a rule such as:
CODE EXAMPLE
Only one job isolated to this actor
executes at a time.
But it cannot understand a product rule such as:
CODE EXAMPLE
If the user selected Rocket 42
before the network request,
they must still have Rocket 42
selected afterward.
That is application logic.
Swift can protect us from simultaneous unsafe actor-isolated access.
We still have to reason about state changes across suspension points.
23 · TUTORIAL
What Happens When a Job Is Ready to Run?
Suppose a suspended Task becomes ready to continue.
Its continuation now represents eligible work.
EXECUTION DIAGRAM
Task A
SUSPENDED
│
│ awaited operation completes
▼
CONTINUATION READY
│
▼
Eligible Job
That job must execute on an appropriate executor.
If it belongs to an actor:
EXECUTION DIAGRAM
Eligible Actor Job
│
▼
Actor's Serial Executor
If it belongs to the MainActor:
EXECUTION DIAGRAM
MainActor Job
│
▼
MainActor Executor
If it has no actor isolation requirement:
EXECUTION DIAGRAM
Nonisolated Job
│
▼
Global Concurrent Executor
The executor requirement therefore tells Swift something very important about where that runnable work belongs.
24 · TUTORIAL
Then How Does the Executor Reach a Thread?
Eventually the work must leave the abstract Swift concurrency model and become actual machine execution.
Conceptually:
EXECUTION DIAGRAM
Eligible Swift Job
│
▼
Executor
│
▼
Runtime execution machinery
│
▼
System Thread
│
▼
OS Scheduler
│
▼
CPU Core
│
▼
Machine Instructions
The exact runtime implementation is more sophisticated than this teaching diagram, and Swift allows custom executors in modern versions of the language.
But the important architectural distinction remains stable:
The executor belongs to Swift's concurrency model.
The thread belongs to the operating-system execution model.
25 · TUTORIAL
One Executor Can Serve Many Tasks
This is another reason an executor should not be imagined as one Task's personal thread.
Imagine several Tasks all need MainActor isolation.
EXECUTION DIAGRAM
Task A ─────┐
│
Task B ─────┼────▶ MainActor Executor
│
Task C ─────┤
│
Task D ─────┘
The executor coordinates eligible jobs from those Tasks according to the MainActor's execution rules.
Likewise, many Tasks performing nonisolated concurrent work can share the default global concurrent execution resources.
EXECUTION DIAGRAM
Task A ─────┐
Task B ─────┤
Task C ─────┼────▶ Global Concurrent Executor
Task D ─────┤
Task E ─────┘
The architecture is therefore many-to-few rather than one-to-one.
26 · TUTORIAL
This Is Similar to the Lesson We Learned About Threads
Earlier we discovered that having one thousand asynchronous operations does not mean we need one thousand threads.
Now we can extend that principle.
EXECUTION DIAGRAM
Many Tasks
│
▼
Many eligible Jobs
│
▼
Executors
│
▼
Managed execution resources
│
▼
A limited number of threads
│
▼
Finite CPU cores
Each layer reduces the need for application code to directly manage the layer below it.
This is exactly what a good abstraction should do.
27 · TUTORIAL
Executors Are About Eligibility and Isolation
There is a useful shift in vocabulary happening here.
Traditional thread-focused thinking asks:
CODE EXAMPLE
Which thread should this function run on?
Swift Concurrency increasingly encourages:
CODE EXAMPLE
What isolation does this work require?
Which executor is responsible
for eligible work in that domain?
This is a more architectural way to reason about concurrent software.
We describe ownership and isolation first.
The runtime deals with much of the underlying execution machinery.
28 · TUTORIAL
The Default Global Concurrent Executor
Swift also needs somewhere for ordinary concurrent work that has no specific actor-executor requirement.
The runtime provides a default global concurrent executor shared by work without stricter executor requirements.
Conceptually:
EXECUTION DIAGRAM
Nonisolated Job A ──────┐
│
Nonisolated Job B ──────┤
│
Nonisolated Job C ──────┼──▶ GLOBAL CONCURRENT EXECUTOR
│
Nonisolated Job D ──────┘
│
▼
Managed execution
This is another important difference from an actor's serial executor.
The global executor supports concurrent progress for work that is not constrained by one actor's isolation domain.
29 · TUTORIAL
Modern Swift Also Supports Custom Executors
As Swift's concurrency system has evolved, executors have become an increasingly explicit part of the language and runtime.
Swift provides executor protocols, including serial executors and task executors, and modern Swift can express custom executor requirements for specialised execution environments.
You do not need to build a custom executor in order to understand Swift Concurrency.
For most application code, the important concepts remain:
CODE EXAMPLE
MainActor executor
Actor serial executors
Global concurrent execution
Task executor preferences
for specialised cases
But knowing that executors are a real runtime abstraction rather than merely a teaching metaphor is useful.
30 · TUTORIAL
Do I Need to Choose an Executor Every Time I Create a Task?
No.
Ordinary Swift application code normally expresses isolation and concurrency requirements using language features such as:
CODE EXAMPLE
@MainActor
actor
nonisolated
Task
async let
withTaskGroup
Swift then derives the appropriate execution requirements from those declarations and the surrounding isolation context.
The fact that executors exist does not mean every developer should start manually selecting executors throughout their application.
The goal is understanding.
Once we know the executor layer exists, the behavior of actors, MainActor and suspended Task continuations becomes much easier to explain.
31 · TUTORIAL
Executors Do Not Make CPU Work Free
We should carry forward the lesson from the previous article.
Suppose a job begins executing:
CODE EXAMPLE
for item in oneMillionItems {
performExpensiveCalculation(item)
}
The executor has successfully arranged for that job to execute.
But the executor does not magically divide the synchronous loop into tiny perfectly fair pieces.
If that synchronous region runs for a long time, it continues occupying its execution resource during that period.
This is why cooperative concurrency still requires good application architecture.
The executor can choose among eligible jobs.
It cannot make synchronous work that is already executing become inexpensive simply because other Tasks would also like to make progress.
💡 Remember
An executor gives eligible jobs opportunities to execute. It does not automatically time-slice arbitrary synchronous Swift code into tiny pieces.
32 · TUTORIAL
Now Bring Back Our Animation Frames
This fits directly into the responsive-app mental model we have been developing.
Imagine our application has several eligible pieces of work.
EXECUTION DIAGRAM
TIME ─────────────────────────────────────────────▶
UI / MainActor
██ ██ ██ ██
Task A
████ ████
Task B
████ ████
Task C
████
This is conceptual rather than a literal Swift scheduling diagram.
But it expresses what we want from the architecture.
Our application's different pieces of work can make progress over time without one unnecessarily monopolising execution resources required by everything else.
Executors are part of the machinery that allows eligible Swift jobs to participate in that model.
33 · TUTORIAL
The Executor Is the Gateway to Execution
There is a useful way to summarise everything we have learned.
A Task describes asynchronous work.
A job represents a runnable portion of that work.
An executor provides the execution service for eligible jobs.
A system thread provides the operating-system execution context.
The OS scheduler decides when that thread receives processor time.
The CPU executes the machine instructions.
So:
EXECUTION DIAGRAM
TASK
"What asynchronous work exists?"
│
▼
JOB
"What part is runnable now?"
│
▼
EXECUTOR
"Where is this eligible Swift work
allowed / arranged to execute?"
│
▼
THREAD
"What OS execution context
is running instructions?"
│
▼
CPU CORE
"What machine instruction
executes now?"
Those are different layers answering different questions.
34 · TUTORIAL
Why This Matters to an iOS Developer
Without executors, several common Swift Concurrency statements remain vague.
Why does an actor prevent simultaneous access to its isolated state?
Because actor-isolated jobs execute through that actor's serial executor.
Why can another actor operation run while one actor method is suspended?
Because the suspended Task is no longer executing its current job, allowing another eligible actor job to execute through the serial executor.
Why is MainActor related to the main thread without simply being another name for the main thread?
Because MainActor is a Swift isolation domain whose executor connects that isolation requirement to main-thread execution.
Why doesn't every Task need its own thread?
Because eligible Task jobs are coordinated through executors and can share underlying execution resources.
Why can Swift Concurrency be cooperative while the OS scheduler remains preemptive?
Because executors and Task scheduling exist at one layer while system-thread scheduling exists underneath them.
Again, several apparently disconnected rules become one architecture.
35 · TUTORIAL
What to Remember
💡 What to Remember
An executor is a service that executes Swift concurrency jobs.
A Task represents an asynchronous operation across its lifetime. A job represents schedulable work that is currently eligible to execute.
Executors sit between eligible Swift concurrency jobs and the lower-level system execution resources that ultimately run machine instructions.
An executor is not a system thread.
An executor is not a CPU core.
An executor should not simply be treated as another name for a Grand Central Dispatch queue.
Actors use serial executors so actor-isolated jobs execute one at a time.
Serial actor execution does not mean an actor permanently owns one specific system thread.
The MainActor is a Swift isolation domain whose executor is associated with main-thread execution.
Work without stricter actor isolation requirements can use Swift's global concurrent executor.
When a Task suspends, other eligible jobs can use the relevant executor. When the suspended Task becomes ready again, its continuation can become another eligible job.
Executors enforce execution requirements such as actor serialization, but they do not freeze business state across an await.
Executors also do not automatically divide long synchronous computations into small time slices. Cooperative code still requires us to structure expensive work sensibly.
The complete model is:
EXECUTION DIAGRAM
Task
↓
Job
↓
Executor
↓
System Thread
↓
OS Scheduler
↓
CPU Core
↓
Machine Instructions
36 · TUTORIAL
Your Next Move
We now understand what an executor is and why Swift Concurrency needs this layer between Tasks and system threads.
Tasks describe asynchronous work.
Jobs represent runnable pieces of that work.
Executors arrange eligible jobs for execution.
Threads eventually carry those machine instructions onto processor cores.
But executors introduce another important question.
An actor uses a serial executor.
That executor ensures that actor-isolated jobs do not execute simultaneously.
So what exactly happens when several Tasks all want access to the same actor at the same time?
Do they form a queue?
Which one executes first?
What happens when the currently executing actor method reaches an await?
Can another Task enter the actor before the original method finishes?
And if so, how can actors still protect mutable state?
Those questions take us directly into one of the most important concepts in actor-based programming:
actor isolation and reentrancy.
The next article should be:
How Does an Actor Execute Multiple Tasks Safely?
