top of page

01 · INTRODUCTION

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.

02 · TUTORIAL

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:

EXECUTION DIAGRAM

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.

03 · TUTORIAL

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:

CODE EXAMPLE

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:

CODE EXAMPLE

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.

04 · TUTORIAL

Imagine a Real Application

Suppose we are building our Rocket Launch application.

Opening one screen might require several operations:

EXECUTION DIAGRAM

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:

CODE EXAMPLE

Run something on a background thread.

The real problem is describing the relationships between pieces of work.

05 · TUTORIAL

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.

EXECUTION DIAGRAM

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:

CODE EXAMPLE

HOW MUCH WORK EXISTS?

and:

CODE EXAMPLE

HOW MANY THREADS EXIST?

Those numbers should not have to be equal.

06 · TUTORIAL

This Is the Fundamental Shift

Imagine our application has one thousand pieces of asynchronous work.

The old naive model would be:

EXECUTION DIAGRAM

1000 operations
      │
      ▼
1000 threads

But what we really want is:

EXECUTION DIAGRAM

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.

07 · TUTORIAL

Enter the Swift Task

Swift gives us a new way of representing asynchronous work.

CODE EXAMPLE

Task {
    await loadLaunches()
}

The Task does not mean:

CODE EXAMPLE

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:

CODE EXAMPLE

Task A
Task B
Task C
Task D
Task E
Task F
Task G
Task H
...

without requiring:

CODE EXAMPLE

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.

08 · TUTORIAL

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:

CODE EXAMPLE

SWIFT ASYNCHRONOUS WORK

and:

CODE EXAMPLE

SYSTEM THREAD EXECUTION

This is one of the responsibilities of the concurrency runtime and executor system.

A useful simplified model is:

EXECUTION DIAGRAM

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.

09 · TUTORIAL

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:

EXECUTION DIAGRAM

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.

10 · TUTORIAL

This Is Why await Matters

Many developers first learn await as syntax:

CODE EXAMPLE

let launches = try await api.loadLaunches()

They learn the rule:

CODE EXAMPLE

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:

EXECUTION DIAGRAM

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.

11 · TUTORIAL

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:

EXECUTION DIAGRAM

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.

12 · TUTORIAL

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:

EXECUTION DIAGRAM

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.

13 · TUTORIAL

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:

EXECUTION DIAGRAM

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.

14 · TUTORIAL

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.

EXECUTION DIAGRAM

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.

15 · TUTORIAL

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.

CODE EXAMPLE

@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:

EXECUTION DIAGRAM

Swift Concurrency Runtime

        ├── MainActor execution
        │
        ├── Other actor execution
        │
        └── Nonisolated concurrent work
                    │
                    ▼
             Execution resources
                    │
                    ▼
              System threads

16 · TUTORIAL

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:

CODE EXAMPLE

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.

17 · TUTORIAL

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:

CODE EXAMPLE

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:

CODE EXAMPLE

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.

18 · TUTORIAL

Swift Concurrency Is Therefore More Than New Syntax

This is why reducing Swift Concurrency to:

CODE EXAMPLE

async / await

misses most of the story.

Yes, we have new syntax.

CODE EXAMPLE

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.

EXECUTION DIAGRAM

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.

19 · TUTORIAL

It Changes How We Describe Our Application

Consider an old mental model:

CODE EXAMPLE

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:

CODE EXAMPLE

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.

20 · TUTORIAL

Structured Concurrency Requires Runtime Knowledge

Imagine:

CODE EXAMPLE

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:

CODE EXAMPLE

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.

21 · TUTORIAL

Actors Require Runtime Support Too

Now consider:

CODE EXAMPLE

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:

CODE EXAMPLE

launches

belongs to an actor-isolated domain.

That is a language-level concept requiring runtime support.

22 · TUTORIAL

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:

CODE EXAMPLE

Which thread accesses this property?

The modern question increasingly becomes:

CODE EXAMPLE

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.

23 · TUTORIAL

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:

CODE EXAMPLE

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.

24 · TUTORIAL

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.

EXECUTION DIAGRAM

SWIFT TASK

running
   │
   ▼
await
   │
   ▼
may suspend
   │
   ▼
other Swift work can use
available execution resources

versus:

EXECUTION DIAGRAM

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.

25 · TUTORIAL

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.

EXECUTION DIAGRAM

┌─────────────────────────────────────────────┐
│                 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.

26 · TUTORIAL

Why This Matters to an iOS Developer

If you begin learning Swift Concurrency with:

CODE EXAMPLE

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:

CODE EXAMPLE

Work

Lifetime

Dependencies

Suspension

Cancellation

Isolation

Ownership

instead of building every feature around:

CODE EXAMPLE

Which thread?

That is the important transition.

27 · TUTORIAL

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 async and await syntax.

The complete model is:

EXECUTION DIAGRAM

Swift Tasks
     ↓
Jobs
     ↓
Executors
     ↓
System Threads
     ↓
OS Scheduler
     ↓
CPU Cores
     ↓
Machine Instructions

28 · TUTORIAL

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?

bottom of page