Why Does Swift Concurrency Need a Runtime?
Swift Concurrency needs a runtime because Tasks are not threads. Once Swift allows us to create large numbers of asynchronous Tasks that can suspend, resume, wait for other Tasks, inherit priorities and execute within actor isolation, something has to coordinate how all of that work eventually reaches the finite collection of system threads on which machine instructions can actually execute.
That coordinating machinery is part of the Swift Concurrency Runtime.
This is an important moment in our journey because we have already learned that the operating system has a scheduler. We know that iOS can manage threads from many processes and schedule those runnable threads onto a much smaller collection of processor cores.
So it is perfectly reasonable to ask:
If the operating system already has a scheduler, why does Swift need another one?
The answer is that the two systems are managing different things at different levels.
The operating system schedules threads.
Swift Concurrency manages asynchronous program work.
The Swift runtime understands concepts such as Tasks, suspension and executors that the operating-system thread scheduler does not understand as Swift language concepts. Eventually, runnable Swift work still has to execute using system threads, and those threads are still scheduled by the operating system onto CPU cores.
💡 The Answer
The Swift Concurrency Runtime exists to coordinate Swift's higher-level asynchronous work — including Tasks, jobs, executors and suspension — over the lower-level system threads that ultimately execute machine instructions.
The OS scheduler has not been replaced. Swift Concurrency adds another scheduling and coordination layer above it.
We Already Had Concurrency Before Swift Concurrency
This point is essential.
Swift Concurrency did not invent concurrency.
Our applications could already contain multiple threads. Grand Central Dispatch already allowed us to submit work to queues. The operating system already scheduled runnable threads onto processor cores. Applications had been performing networking, database work, image processing and many other operations concurrently for years.
Our existing model looked roughly like this:
APPLICATION WORK
│
▼
GCD / OTHER CONCURRENCY APIs
│
▼
SYSTEM THREADS
│
▼
OS SCHEDULER
│
▼
CPU CORES
│
▼
MACHINE INSTRUCTIONS
This worked.
Millions of applications were built using this architecture.
But “it works” is not the same thing as saying we had reached the best possible programming model.
As applications grew larger, the difficult part was increasingly not merely getting work away from the main thread.
The difficult part was coordinating all of the concurrent work correctly.
The Old Question Was Often: Which Queue?
For many iOS developers, concurrency became heavily associated with Grand Central Dispatch.
We would write code such as:
DispatchQueue.global().async {
let result = performWork()
DispatchQueue.main.async {
updateUI(with: result)
}
}
This is a useful API and it solved a very real problem.
We could submit expensive work away from the main queue and then dispatch UI work back to the main queue.
But notice how much of the architecture is expressed in terms of execution mechanisms:
Which queue?
Main or global?
Serial or concurrent?
Which quality of service?
When do I dispatch back?
Which state is being accessed?
What if this closure outlives its owner?
What if two closures mutate the same value?
How do I cancel this work?
What depends on what?
As the application becomes larger, those questions become distributed across thousands of lines of code.
Concurrency stops being a small implementation detail and becomes an architectural problem.
Imagine a Real Application
Suppose we are building our Rocket Launch application.
Opening one screen might require several operations:
Load Launches
│
├── Download launch data
├── Download rocket data
├── Load astronaut data
├── Read cached favourites
└── Update the interface
Some operations can happen concurrently.
Some depend on other operations completing first.
Some may fail.
Some may become irrelevant if the user leaves the screen.
Some produce values that must eventually update UI state.
Some state must never be mutated concurrently.
This is much more interesting than:
Run something on a background thread.
The real problem is describing the relationships between pieces of work.
What If We Simply Created More Threads?
We have already investigated this possibility.
Imagine every feature simply creates a thread whenever it needs asynchronous work.
Load Profile ─────────▶ Thread 2
Load Image ───────────▶ Thread 3
Load Launches ────────▶ Thread 4
Load Rockets ─────────▶ Thread 5
Load Astronauts ──────▶ Thread 6
Save Cache ───────────▶ Thread 7
Refresh Widget ───────▶ Thread 8
As the application grows, the number of execution contexts can grow with it.
But we already know that threads are not free.
They require stacks and system resources. They have scheduling costs. Creating more threads does not create more processor cores. And a large amount of asynchronous application work spends significant periods waiting rather than actively executing instructions.
We therefore need to separate two ideas:
HOW MUCH WORK EXISTS?
and:
HOW MANY THREADS EXIST?
Those numbers should not have to be equal.
This Is the Fundamental Shift
Imagine our application has one thousand pieces of asynchronous work.
The old naive model would be:
1000 operations
│
▼
1000 threads
But what we really want is:
1000 pieces of asynchronous work
│
▼
CONCURRENCY SYSTEM
│
▼
Managed execution
│
▼
A limited set of threads
│
▼
CPU cores
This is the architectural space in which Swift Concurrency operates.
Enter the Swift Task
Swift gives us a new way of representing asynchronous work.
Task {
await loadLaunches()
}
The Task does not mean:
Create a brand-new operating-system thread
and permanently attach it to loadLaunches().
It represents asynchronous work managed by Swift's concurrency system.
This immediately gives the runtime more flexibility.
We can have many Tasks:
Task A
Task B
Task C
Task D
Task E
Task F
Task G
Task H
...
without requiring:
Thread A
Thread B
Thread C
Thread D
Thread E
Thread F
Thread G
Thread H
...
Tasks and threads are different resources at different abstraction layers.
But a Task Eventually Has to Execute
This raises an obvious question.
If a Task is not a thread, how does its Swift code actually run?
Eventually, some synchronous portion of that Task's work must execute as machine instructions on a processor core.
So there must be a bridge between:
SWIFT ASYNCHRONOUS WORK
and:
SYSTEM THREAD EXECUTION
This is one of the responsibilities of the concurrency runtime and executor system.
A useful simplified model is:
Swift Task
│
▼
Runnable Job
│
▼
Executor
│
▼
System Thread
│
▼
OS Scheduler
│
▼
CPU Core
We have added new machinery, but we have not removed any of the computing foundations we previously learned.
What Is a Job?
This is a useful word to introduce because it prevents us from imagining that an entire Task has to occupy a thread from beginning to end.
A Task has a lifetime.
During that lifetime it may execute, reach an asynchronous suspension point, stop executing, and later continue.
The runtime can represent pieces of eligible synchronous work as jobs that executors can run.
For our mental model:
TASK
Start
│
▼
Runnable Work
│
▼
await something()
│
▼
SUSPENDED
│
│
│ asynchronous operation completes
│
▼
Runnable Work
│
▼
Continue Task
The Task persists across this journey.
A thread does not need to remain permanently attached to it while the Task is suspended.
This Is Why await Matters
Many developers first learn await as syntax:
let launches = try await api.loadLaunches()
They learn the rule:
You must write await
when calling an async function.
That is correct, but it does not explain why the keyword is architecturally important.
An await marks a point where an asynchronous function may suspend.
That means execution does not necessarily have to sit on a thread doing nothing while the operation waits.
Conceptually:
Task A
████████████████
│
▼
await
│
▼
SUSPEND
Execution resource becomes
available for other work
Task B
████████████████
When the operation Task A is waiting for becomes ready, Task A can later become eligible to continue.
This is dramatically different from thinking that every unfinished asynchronous operation must own a blocked thread.
The Task Remembers Where to Continue
This connects beautifully to the “save point” mental model we have used when discussing Swift Concurrency.
Imagine playing a game.
You reach a save point:
LEVEL 4
🚀
│
▼
[ SAVE POINT ]
You do not need to keep playing the game continuously for your progress to exist.
Your state can be preserved so that you can return later and continue from the appropriate point.
Suspending asynchronous work gives us a loosely similar conceptual model.
The Task does not need to continuously occupy a thread merely because it has more work to perform in the future.
Its continuation state can be preserved so that execution can resume when the awaited operation makes that continuation eligible.
This Is Where Cooperative Concurrency Appears
We can now introduce one of the most important words in modern Swift concurrency:
cooperative.
The Swift concurrency runtime works best when asynchronous code cooperates with the execution model rather than monopolising execution resources.
Imagine Task A performs a small amount of work and reaches an asynchronous suspension point:
Task A
████████
│
▼
suspend
Task B
████████
│
▼
suspend
Task C
████████
Different pieces of application work can make progress without every piece of unfinished work requiring its own dedicated thread.
This is much closer to the concurrency model we actually want for modern applications.
But Cooperative Does Not Mean the OS Stopped Being Preemptive
This distinction is worth learning properly because it causes enormous confusion.
The operating system still schedules threads.
The OS can still preempt a running thread.
Swift Concurrency did not replace the kernel scheduler.
We have two different layers:
SWIFT CONCURRENCY LAYER
Tasks
↓
Jobs
↓
Executors
↓
Cooperative execution model
OPERATING SYSTEM LAYER
System Threads
↓
Preemptive OS Scheduler
↓
CPU Cores
There is no contradiction.
Swift's cooperative concurrency model exists above the operating system's preemptive thread scheduler.
What Does an Executor Do?
Now another Swift Concurrency term becomes much easier to place.
An executor is part of the machinery responsible for arranging the execution of jobs.
You can think of an executor as answering a question similar to:
Which eligible job should execute within this execution domain?
This is deliberately different from saying:
Which CPU core should execute this thread?
The latter is an operating-system scheduling problem.
The former belongs to Swift's concurrency model.
Swift Jobs
│
▼
Executor
│
▼
Underlying execution resources
│
▼
System Threads
│
▼
OS Scheduler
│
▼
CPU
This separation is exactly why we need to stop treating executors and threads as synonyms.
There Are Executors, Not One Giant Executor
It is also useful to avoid imagining the Swift Concurrency Runtime as one enormous singleton object with one queue of every line of asynchronous Swift code.
Different isolation domains can be associated with different executor behavior.
The most obvious example for an iOS developer is the MainActor.
@MainActor
final class LaunchModel {
var launches: [Launch] = []
}
This type is isolated to the MainActor.
Work requiring that isolation must respect the MainActor's execution domain.
Other nonisolated concurrent work does not automatically belong to that same domain.
So our mental model is becoming richer:
Swift Concurrency Runtime
├── MainActor execution
│
├── Other actor execution
│
└── Nonisolated concurrent work
│
▼
Execution resources
│
▼
System threads
The Runtime Can Understand Things the OS Scheduler Cannot
This is perhaps the clearest answer to the title of this article.
The operating-system scheduler understands operating-system execution resources.
It understands threads, priorities, runnable states and processor resources.
But it does not understand your Swift program in the same way the Swift concurrency system does.
The OS scheduler does not reason in terms of:
Swift Task
async let
TaskGroup
Actor isolation
MainActor
Swift child Task
Swift Task cancellation
await suspension
Those concepts belong to the Swift programming and concurrency model.
Something inside the running Swift program therefore needs to coordinate them.
That is why a language-level concurrency runtime exists.
The Runtime Exists Inside Your Running Program
The word runtime can make this system sound more mysterious than it really is.
It can sound as though there is another application somewhere inside iOS called:
Swift Concurrency Runtime.app
and our application somehow sends requests to it.
That is the wrong mental model.
The concurrency runtime is runtime machinery used by the Swift program while it is executing.
A useful conceptual analogy is to imagine that we had written infrastructure ourselves:
final class ConcurrentTaskManager {
static let shared = ConcurrentTaskManager()
func submit(_ work: Work) {
// manage eligible work
}
}
and then throughout our application we continuously interacted with that management system.
The real Swift Concurrency Runtime is vastly more sophisticated than this imaginary class, but the analogy removes some of the mystery from the word runtime.
It is machinery that exists to support the behavior of Swift concurrency features while the program is running.
Swift Concurrency Is Therefore More Than New Syntax
This is why reducing Swift Concurrency to:
async / await
misses most of the story.
Yes, we have new syntax.
async
await
Task
async let
withTaskGroup
actor
@MainActor
But those language features describe a new concurrency model.
And that model requires runtime support.
The language and runtime work together.
SWIFT LANGUAGE
async
await
Task
actor
@MainActor
structured concurrency
│
▼
SWIFT CONCURRENCY RUNTIME
Tasks
Jobs
Executors
Suspension
Scheduling support
│
▼
SYSTEM THREADS
│
▼
OS SCHEDULER
│
▼
CPU CORES
This is why Swift Concurrency should not be learned as though Apple simply released another utility framework.
It Changes How We Describe Our Application
Consider an old mental model:
Networking?
Put it on a background queue.
Image processing?
Put it on a background queue.
Finished?
Dispatch back to main.
Now compare that with the questions we can ask in a structured concurrency system:
What is the lifetime of this work?
Who owns this Task?
Can these operations run concurrently?
Does this operation depend on another?
Can this Task be cancelled?
Where can this function suspend?
What state does this code access?
Which actor isolates that state?
Does this work belong to MainActor?
These are application-architecture questions.
They describe the relationships between our features rather than merely telling a queue where to execute a closure.
Structured Concurrency Requires Runtime Knowledge
Imagine:
async let launches = loadLaunches()
async let rockets = loadRockets()
let result = try await (launches, rockets)
There is structure here.
These child operations belong to a surrounding scope.
The language understands their relationship to the parent operation.
The runtime can participate in managing that work according to Swift's structured-concurrency rules.
Compare this with simply firing unrelated closures into global queues:
DispatchQueue.global().async {
loadLaunches()
}
DispatchQueue.global().async {
loadRockets()
}
The two pieces of code may execute concurrently, but the structure of their relationship is not expressed in the same way.
Swift Concurrency gives the language and runtime more information about the shape of asynchronous work.
Actors Require Runtime Support Too
Now consider:
actor LaunchStore {
private var launches: [Launch] = []
func save(_ launch: Launch) {
launches.append(launch)
}
}
The actor is not simply a class with a fashionable keyword in front of it.
It introduces an isolation domain.
Access to actor-isolated state must follow Swift's concurrency rules.
Eligible work for that actor must be coordinated through its executor.
Again, the operating-system scheduler cannot provide this Swift-level semantic guarantee by itself.
The OS knows about threads.
Swift knows that:
launches
belongs to an actor-isolated domain.
That is a language-level concept requiring runtime support.
We Are Moving From Threads to Isolation
This is one of the biggest changes in how we should think as iOS developers.
The old question was often:
Which thread accesses this property?
The modern question increasingly becomes:
What isolates this property?
Those questions are related, but they are not the same.
A thread describes an execution resource.
Isolation describes a correctness boundary in our program.
That is a much more useful level at which to design application state.
The Runtime Does Not Make Bad Code Magically Cooperative
There is an important warning here.
Introducing Swift Concurrency does not mean developers can write arbitrary expensive synchronous code and expect the runtime to continuously interrupt it at convenient Swift source-code boundaries.
Imagine:
func calculateEverything() {
for _ in 0..<1_000_000_000 {
performExpensiveCalculation()
}
}
If this is synchronous CPU-bound work executing as part of a concurrency job, it does not become magically cheap because a Task exists somewhere above it.
The runtime cannot simply treat every Swift source line as an automatic suspension point.
This is why the word cooperative matters.
Our code must participate correctly in the concurrency model.
Suspension Is Not Preemption
This distinction deserves to be explicit.
A Swift Task reaching an await where it must suspend is different from the operating system forcibly preempting a running thread.
SWIFT TASK
running
│
▼
await
│
▼
may suspend
│
▼
other Swift work can use
available execution resources
versus:
SYSTEM THREAD
running
│
▼
OS scheduler preempts
│
▼
thread state preserved
│
▼
another thread executes
Both mechanisms can contribute to concurrency.
They operate at different layers and for different reasons.
Now We Can Draw the Whole Machine
We started this article series with processes and threads.
We can now place Swift Concurrency above everything we have learned.
┌─────────────────────────────────────────────┐
│ OUR IOS APP │
│ │
│ Swift Tasks │
│ │ │
│ ▼ │
│ Jobs │
│ │ │
│ ▼ │
│ Executors │
│ │ │
│ ▼ │
│ Swift Concurrency Runtime │
│ │ │
│ ▼ │
│ System Threads │
│ │
│ APP PROCESS │
└─────────────────────┬───────────────────────┘
│
▼
OS SCHEDULER
│
▼
CPU CORES
│
▼
MACHINE INSTRUCTIONS
That is the architecture we have been working toward.
Swift Concurrency exists above the operating-system thread scheduler.
It gives Swift a way to reason about asynchronous work using concepts that are much closer to the architecture of our program than raw threads are.
Why This Matters to an iOS Developer
If you begin learning Swift Concurrency with:
func load() async {
await something()
}
it is very easy to believe that you are simply learning a cleaner syntax for callbacks.
That massively understates what has changed.
We now have a language and runtime model that understands asynchronous Tasks, suspension, structured relationships between Tasks, executors and actor isolation.
The operating system still provides the lower-level mechanisms.
Processes still exist.
Threads still exist.
The OS scheduler still exists.
CPU cores still execute machine instructions.
But as application developers, we can increasingly describe our software in terms of:
Work
Lifetime
Dependencies
Suspension
Cancellation
Isolation
Ownership
instead of building every feature around:
Which thread?
That is the important transition.
What to Remember
💡 What to Remember
Swift Concurrency needs a runtime because Swift Tasks and system threads are not the same thing.
The operating system scheduler manages runnable system threads and schedules them onto available processor cores.
The Swift Concurrency Runtime operates at a higher level and supports Swift concepts such as Tasks, jobs, executors, suspension and actor isolation.
A Swift Task can exist without permanently owning one system thread for its entire lifetime.
When asynchronous work reaches a suspension point and cannot continue immediately, the Task can suspend rather than requiring a thread to remain blocked merely to represent unfinished work.
When the Task becomes eligible to continue, its work can later be scheduled for execution again.
Executors coordinate the execution of eligible Swift concurrency jobs. They should not be confused with operating-system threads or CPU cores.
Swift's cooperative concurrency model exists above the operating system's preemptive thread scheduler. Both systems continue to operate.
Actor isolation allows us to reason about ownership and safe access to mutable state at a higher level than manually reasoning about which thread happens to touch a property.
Swift Concurrency therefore represents an architectural change, not merely the addition of
asyncandawaitsyntax.The complete model is:
Swift Tasks ↓ Jobs ↓ Executors ↓ System Threads ↓ OS Scheduler ↓ CPU Cores ↓ Machine Instructions
Your Next Move
We now know why Swift needs a concurrency runtime even though the operating system already has a thread scheduler.
The two systems solve problems at different layers.
The operating system schedules threads.
Swift Concurrency coordinates asynchronous program work.
And one of the most important abilities this new architecture gives us is the ability for a Task to stop executing without requiring its asynchronous lifetime to end.
We call that:
suspension.
But suspension is frequently misunderstood.
What exactly gets suspended?
Does the thread suspend?
Does await immediately cause a context switch?
Where does the Task go while it is waiting?
How can another piece of work execute if the original Task has not finished?
And how can the Task later continue from the point where it stopped?
Those questions take us directly into the mechanism that makes cooperative asynchronous execution possible.
The next article should be:
What Actually Happens When a Swift Task Suspends?
