top of page

01 · INTRODUCTION

How Does the OS Switch Between Threads?

Once you understand that your iOS application runs inside a process, and that the process contains threads, the next question becomes almost unavoidable:

Who decides which thread gets to use the processor?

This is one of the most useful questions you can ask when learning concurrency because it explains something that can otherwise feel almost impossible to visualise. We say that several threads are executing “concurrently,” but if we deliberately simplify our computer to contain one processor core, that core can only execute one stream of machine instructions at a time.

So how can three threads all make progress?

The answer is that the operating system switches between them.

Understanding how and why that happens gives us the foundation for understanding race conditions, thread safety and, eventually, one of the most important differences between traditional thread scheduling and the cooperative execution model used by Swift Concurrency.

02 · TUTORIAL

Start With One CPU Core

As we did in the previous article, temporarily forget that modern iPhones contain processors with multiple cores.

Multiple cores introduce parallelism, and we do not need parallelism to understand concurrency.

Instead, imagine a computer with one processor core.

Now imagine our application process contains three threads.

EXECUTION DIAGRAM

┌─────────────────────────────────────────┐
│              MY APP                     │
│              PROCESS                    │
│                                         │
│        Application Memory               │
│                                         │
│        Main Thread ───────────────▶      │
│        Thread 2    ───────────────▶      │
│        Thread 3    ───────────────▶      │
│                                         │
└─────────────────────────────────────────┘

                         │
                         ▼

                    ┌─────────┐
                    │   CPU   │
                    │  CORE   │
                    └─────────┘

There is an obvious problem.

We have three threads containing work that wants to execute, but only one processor core on which to execute it.

The operating system needs to decide who gets a turn.

03 · TUTORIAL

The OS Scheduler Decides Which Runnable Thread Executes

At the operating-system level, scheduling machinery determines which runnable thread should execute on an available processor core.

The important word here is runnable.

A thread may exist without currently being able to make progress. It might be waiting for some event, sleeping, blocked waiting for a resource, or otherwise not ready to execute. The scheduler is interested in threads that currently have executable work and are eligible to run.

Our simplified picture therefore becomes:

EXECUTION DIAGRAM

Runnable Threads

Main Thread ───────┐
                   │
Thread 2 ──────────┼──▶ OS Scheduler ──▶ CPU Core
                   │
Thread 3 ──────────┘

The CPU core does not execute all three at once.

One thread is selected.

Its instructions execute.

At some later point, another thread may be selected.

This switching is what allows several threads to make progress during the same period of time even when we only have one processor core.

That is concurrency in its most useful form for our mental model.

04 · TUTORIAL

The Thread Does Not Decide When Its Turn Ends

This is an extremely important idea.

With traditional operating-system thread scheduling, the executing thread does not generally control exactly when the operating system will stop scheduling it.

The OS uses preemptive scheduling.

Conceptually, imagine Thread 1 contains a long sequence of machine instructions:

CODE EXAMPLE

THREAD 1

Instruction 1
Instruction 2
Instruction 3
Instruction 4
Instruction 5
Instruction 6
Instruction 7
Instruction 8
Instruction 9
...

The thread begins executing on the CPU.

EXECUTION DIAGRAM

CPU CORE

Instruction 1
      ↓
Instruction 2
      ↓
Instruction 3
      ↓
Instruction 4

Then the operating system may schedule something else.

Thread 1 has not necessarily finished its entire workload. It has simply stopped executing for now.

Another runnable thread can then use the processor.

CODE EXAMPLE

THREAD 1                 THREAD 2

Instruction 1
Instruction 2
Instruction 3
Instruction 4
     PAUSED

                         Instruction 1
                         Instruction 2
                         Instruction 3
                         Instruction 4

Later, Thread 1 may run again and continue from its saved execution state.

This is the behaviour we need to understand when reasoning about traditional multithreaded code.

05 · TUTORIAL

The OS Must Remember Where the Thread Was

Imagine reading a book and somebody suddenly tells you to stop.

You would need to remember the page you were reading before handing the book to somebody else. Otherwise, when you received the book again, you would have no idea where to continue.

A thread has the same basic problem.

When the operating system switches execution away from one thread, enough of that thread's execution state must be preserved so that it can later continue correctly.

At the processor level, execution state includes things such as register values, the stack pointer and the instruction location required to resume execution.

We do not need to become kernel engineers to understand the important point:

💡 Important Idea

A paused thread can later continue from where its execution left off.

Conceptually:

EXECUTION DIAGRAM

THREAD 1

Instruction 1
Instruction 2
Instruction 3
Instruction 4
       ↓
   SAVE STATE
       │
       │
       ▼

THREAD 2 EXECUTES

       │
       │
       ▼

RESTORE THREAD 1
       ↓
Instruction 5
Instruction 6
Instruction 7

The act of moving execution from one thread to another involves a context switch.

06 · TUTORIAL

What Is a Context Switch?

A context switch is the operating system changing the execution context being run on a processor core.

In our simplified model, the CPU was executing Thread 1.

EXECUTION DIAGRAM

CPU
 ↓
Thread 1

The OS switches execution.

EXECUTION DIAGRAM

CPU
 ↓
Thread 2

To make that possible, the system preserves the state required for Thread 1 to continue later and establishes the state required to execute Thread 2.

Later it may switch again.

EXECUTION DIAGRAM

TIME ─────────────────────────────────────────▶

CPU CORE

Thread 1     Thread 2     Thread 3     Thread 1
████████     ████████     ████████     ████████

Notice what this diagram actually shows.

Thread 1, Thread 2 and Thread 3 are not executing simultaneously.

They are taking turns.

Yet if those turns happen rapidly enough, all three threads make progress during the same overall period.

That is why concurrency does not require parallelism.

07 · TUTORIAL

Now Put the Threads Back Inside Their Process

This is where our previous article becomes useful.

Threads do not exist as some mysterious collection of lines floating around your application.

They belong to processes.

Our application might look like this:

EXECUTION DIAGRAM

┌─────────────────────────────────────────┐
│              MY APP PROCESS             │
│                                         │
│    Main Thread                          │
│    Thread 2                             │
│    Thread 3                             │
│                                         │
└─────────────────────────────────────────┘

Another application has another process with its own threads.

EXECUTION DIAGRAM

┌─────────────────────────────────────────┐
│            ANOTHER APP PROCESS          │
│                                         │
│    Main Thread                          │
│    Thread 2                             │
│                                         │
└─────────────────────────────────────────┘

The operating system is responsible for scheduling runnable threads across the system onto the available processor cores.

So although threads belong to their respective processes, processor time is a system resource being coordinated by the OS.

That gives us a more complete mental model:

EXECUTION DIAGRAM

APP PROCESS A              APP PROCESS B

Main Thread                Main Thread
Thread 2                   Thread 2
Thread 3
     │                         │
     └──────────┬──────────────┘
                ▼
          OS SCHEDULER
                │
                ▼
           CPU CORES

This is essentially how multitasking and multithreading meet.

08 · TUTORIAL

Why Does the OS Switch Between Threads?

Because many pieces of software need to make progress.

Your app needs processor time. Another app may need processor time. System services need processor time. And within your own process, several threads may be runnable.

If one thread were simply allowed to take a processor core forever, other runnable work could be prevented from progressing.

The operating system therefore manages processor time across the runnable work in the system, taking into account scheduling policies, priorities and other implementation details.

For us as application developers, the crucial lesson is simpler:

💡 Important Idea

Your thread does not own the CPU.

It receives opportunities to execute.

That difference matters enormously.

09 · TUTORIAL

This Explains Why Thread Timing Is Not Predictable

Now we can begin connecting operating-system scheduling to the bugs we encounter in application code.

Imagine we submit two pieces of work:

CODE EXAMPLE

DispatchQueue.global().async {
    print("A")
}

DispatchQueue.global().async {
    print("B")
}

A common beginner assumption is that because the first block appears first in the Swift source file, its work must finish before the second block.

That is not a safe assumption for independently scheduled concurrent work.

Once work can execute concurrently, its actual execution depends on scheduling and the synchronization guarantees of the APIs involved.

This means we should stop reasoning about concurrent code as though the vertical order in our source file completely determines the order in which independent work will finish.

That assumption belongs to sequential code.

Concurrency changes the model.

10 · TUTORIAL

Now Consider Shared State

This becomes much more important when two execution contexts reach the same mutable value.

Consider:

CODE EXAMPLE

var counter = 0

Now imagine two threads both perform:

CODE EXAMPLE

counter += 1

At the Swift level we see one line.

But conceptually, the operation involves several stages:

CODE EXAMPLE

READ counter
ADD 1
WRITE counter

Imagine Thread 1 begins:

EXECUTION DIAGRAM

THREAD 1

READ counter → 10
ADD 1 → 11

Before its result has been safely coordinated with other work, another execution context may perform conflicting operations against the same state.

For example:

EXECUTION DIAGRAM

THREAD 1                     THREAD 2

READ counter → 10
ADD 1 → 11
      │
      │
      │                      READ counter → 10
      │                      ADD 1 → 11
      │                      WRITE 11
      │
WRITE 11

Both operations intended to increase the value.

The final value is still 11.

This is one form of the lost-update problem.

Once you understand that concurrent execution can interleave, the result no longer seems magical.

The program failed to synchronise access to shared mutable state.

11 · TUTORIAL

Your Swift Lines Are Not Scheduling Boundaries

This is another idea worth committing to memory.

Suppose you write:

CODE EXAMPLE

let oldValue = counter
let newValue = oldValue + 1
counter = newValue

As a human reading the source file, those lines feel like one continuous thought.

The operating system does not schedule your program according to the visual grouping of Swift statements in Xcode.

And each Swift statement can itself compile into multiple lower-level instructions.

So this mental model is dangerous:

CODE EXAMPLE

My Swift line executes
My next Swift line executes
My next Swift line executes
Then something else gets a turn

There is no general guarantee like that for preemptively scheduled thread execution.

This is why concurrent programming requires us to think about synchronisation, ownership and isolation, rather than assuming adjacent source-code statements form an indivisible unit.

12 · TUTORIAL

Run a Broken Experiment

You can make this visible yourself.

CODE EXAMPLE

import Foundation

final class Counter {
    var value = 0
}

let counter = Counter()
let group = DispatchGroup()

for _ in 0..<10_000 {
    group.enter()

    DispatchQueue.global().async {
        counter.value += 1
        group.leave()
    }
}

group.wait()

print("Expected: 10000")
print("Actual: \(counter.value)")

This code deliberately performs unsynchronised concurrent access to mutable state.

Run it several times.

The point is not to learn what particular number appears.

The point is to recognise that the program contains no mechanism guaranteeing that every read-modify-write operation occurs safely with respect to the others.

Once you have seen this behaviour yourself, the phrase race condition stops being a vague interview definition.

You can begin to understand the machine underneath it.

13 · TUTORIAL

Context Switching Is Useful, but It Is Not Free

There is another reason we should understand thread switching.

A context switch requires work.

The system must stop executing one context, preserve the required state, prepare another context and continue execution.

Threads themselves also consume resources, including stack space and kernel bookkeeping.

This is one reason that “just create more threads” is not a sensible general concurrency strategy.

If ten operations need to make progress, creating ten new threads is not automatically better.

If one thousand operations need to make progress, creating one thousand threads would be an extremely poor abstraction for most application workloads.

This brings us directly toward the motivation for higher-level concurrency systems.

14 · TUTORIAL

GCD Already Started Hiding Thread Management From Us

Grand Central Dispatch moved iOS developers away from thinking that every piece of asynchronous work should correspond to a manually created thread.

Instead of creating a thread ourselves, we submit work:

CODE EXAMPLE

DispatchQueue.global().async {
    performWork()
}

We describe work to GCD.

GCD and the operating system coordinate the underlying execution resources.

That was an important abstraction because application developers usually care more about what work needs to happen than about manually creating and destroying the system threads used to execute it.

Swift Concurrency takes that separation much further.

15 · TUTORIAL

Now Compare This With Swift Concurrency

This is where understanding OS thread scheduling becomes extremely valuable.

Traditional system threads are preemptively scheduled.

The operating system can stop scheduling one thread and schedule another according to the system's scheduling rules.

Swift Concurrency introduces a higher-level task model that uses cooperative scheduling.

These are not the same thing.

A Swift task is not a thread.

A runnable synchronous portion of task work is commonly described as a job, and executors arrange eligible jobs for execution on underlying threads.

A useful simplified model is:

EXECUTION DIAGRAM

Swift Tasks
     │
     ▼
    Jobs
     │
     ▼
 Executors
     │
     ▼
System Threads
     │
     ▼
OS Scheduling
     │
     ▼
Processor Cores

Notice that Swift Concurrency has not deleted the operating system.

Threads still exist underneath.

The OS still schedules those threads.

Processor cores still execute machine instructions.

Swift has added a higher-level system above those resources.

16 · TUTORIAL

What Does Cooperative Mean?

This is one of the most important concepts in modern Swift concurrency.

With preemptive thread scheduling, the operating system controls when a system thread receives processor time and when another thread may run.

Within Swift Concurrency's task model, asynchronous work can reach suspension points where the current task no longer needs to occupy the execution resource while it waits.

For example:

CODE EXAMPLE

let image = try await loadImage()

The await does not mean:

CODE EXAMPLE

Switch this thread now.

It marks a call where the current task may suspend.

If suspension occurs, other eligible Swift concurrency work can use execution resources rather than leaving a thread blocked purely because this task is waiting for an asynchronous result.

This is a fundamentally better abstraction for application developers.

We describe asynchronous work and its suspension points.

The runtime coordinates how eligible work uses the underlying execution resources.

17 · TUTORIAL

But Cooperative Code Can Still Block

There is an important warning here.

Swift Concurrency does not mean that expensive synchronous code suddenly stops being expensive.

Consider:

CODE EXAMPLE

@MainActor
func performHugeCalculation() {
    for _ in 0..<500_000_000 {
        calculate()
    }
}

If that synchronous work executes for a long time on the main actor, it can still prevent other main-actor work from making timely progress.

There is no magical async keyword that makes badly placed synchronous work disappear.

This is why cooperative is such an important word.

Our architecture needs to cooperate with the concurrency system.

We need to understand where work belongs, where suspension can happen, what state is isolated and which operations are too expensive to perform in a particular execution context.

Swift Concurrency gives us a much better system.

We still need to use it correctly.

18 · TUTORIAL

Preemptive Threads and Cooperative Tasks Are Different Layers

It is worth seeing the distinction clearly.

At the operating-system layer:

EXECUTION DIAGRAM

THREAD 1
████████

        THREAD 2
        ████████

                THREAD 3
                ████████

                        THREAD 1
                        ████████

The OS schedules threads onto processor cores.

At the Swift Concurrency layer:

EXECUTION DIAGRAM

TASK
  │
  ▼
JOB
████████
       │
       ▼
     await
       │
   may suspend
       │
       ▼
other eligible work
can make progress
       │
       ▼
later continuation
████████

These systems coexist.

Swift Concurrency manages higher-level asynchronous work.

Eventually that work still has to execute using system threads.

And those threads are still scheduled by the operating system.

That is the complete picture we are trying to build.

19 · TUTORIAL

Why This Matters to an iOS Developer

Without this model, concurrency can sound like a collection of unrelated warnings:

Don't block the main thread.

Don't mutate this from multiple threads.

Task doesn't create a thread.

await doesn't mean background thread.

Actors serialize isolated access.

Swift Concurrency is cooperative.

Each statement is true in its proper context, but memorising them individually does not give you a system.

Once you understand thread scheduling, they begin connecting.

Your application runs in a process.

The process contains system threads.

The OS schedules runnable threads onto processor cores.

Those threads execute machine instructions.

Traditional concurrent access to shared state can produce unpredictable interleavings unless correctly synchronised.

Swift Concurrency gives us a higher-level task, executor and isolation model above those threads.

Now we have one architecture rather than six disconnected rules.

20 · TUTORIAL

What to Remember

💡 What to Remember

The operating system is responsible for scheduling runnable system threads onto available processor cores.

On our simplified single-core machine, only one thread executes on that core at a particular instant, but the OS can switch between threads so that several make progress during the same period of time.

That is concurrency without parallelism.

When execution switches away from a thread, enough execution state is preserved for that thread to continue correctly later. Switching execution contexts is known as a context switch.

Traditional system-thread scheduling is preemptive. Your Swift source-code lines do not define where the OS is allowed to switch between threads.

That is one reason unsynchronised shared mutable state is dangerous. Operations from different execution contexts can interleave in ways your source code does not visually reveal.

Grand Central Dispatch gave us a higher-level mechanism for submitting work without manually managing every thread.

Swift Concurrency goes further by introducing tasks, jobs, executors, suspension, isolation and cooperative scheduling above the underlying thread system.

But the operating system has not disappeared.

At the bottom of our model, system threads are still scheduled onto processor cores.

21 · TUTORIAL

Your Next Move

Now that we understand that the operating system can select a thread and give it access to processor time, there is an obvious question left unanswered.

What actually happens when that thread reaches the processor?

A thread is not Swift source code.

The CPU does not understand:

CODE EXAMPLE

counter += 1

It executes machine instructions.

So our next step is to follow a piece of Swift through compilation and ask exactly what a processor core is doing when it executes the machine instructions associated with one thread.

That is the next article:

How Does the CPU Process One Thread of Machine Instructions?

bottom of page