01 · INTRODUCTION
What Plugs Into the Processor: a Process or the Thread?
The thread.
When the operating system schedules work onto a CPU core, it is a thread's execution context that runs. The process does not get inserted into the processor as one giant executable object.
The process provides the environment in which the application exists: its virtual address space, memory and other resources. Inside that process are one or more threads. Those threads provide the execution contexts through which the application's machine instructions can actually run on processor cores.
This distinction is extraordinarily useful because developers commonly hear phrases such as “the processor is running my app” or “the CPU is executing the process.” Those descriptions are convenient at a high level, but when we zoom into the machinery underneath concurrency, we need a more precise picture.
💡 The Answer
A thread is scheduled onto a processor core.
The process provides the surrounding execution environment and memory belonging to the application.
02 · TUTORIAL
Start With the Diagram We Already Know
Our iOS application runs inside a process.
EXECUTION DIAGRAM
┌─────────────────────────────────────────┐
│ MY APP PROCESS │
│ │
│ Application Memory │
│ │
│ Main Thread │
│ Thread 2 │
│ Thread 3 │
│ │
└─────────────────────────────────────────┘
The process is our application's operating-system container.
It gives the running application an environment in which its code and data can exist. It has a virtual address space and owns resources associated with the running program.
But the process itself is not the stream of execution.
The threads are.
So when we add the processor to our diagram, we should not draw this:
EXECUTION DIAGRAM
┌───────────────────────────────┐
│ APP PROCESS │
│ │
│ Memory │
│ Main Thread │
│ Thread 2 │
│ Thread 3 │
│ │
└───────────────┬───────────────┘
│
▼
CPU CORE
That diagram can accidentally suggest that the entire process is fed into the processor.
A better mental model is:
EXECUTION DIAGRAM
┌─────────────────────────────────────────┐
│ MY APP PROCESS │
│ │
│ Application Memory │
│ │
│ Main Thread ─────────────┐ │
│ Thread 2 │ │
│ Thread 3 │ │
│ │ │
└───────────────────────────────────┼──────┘
│
▼
OS Scheduler
│
▼
CPU Core
One runnable thread receives an opportunity to execute.
03 · TUTORIAL
The Process and the Thread Have Different Jobs
The easiest way to remember the distinction is to give each concept a responsibility.
EXECUTION DIAGRAM
PROCESS
Provides the environment
for the running application
│
│ contains
▼
THREADS
Provide execution contexts
within that process
│
│ scheduled onto
▼
CPU CORES
Execute machine instructions
The process answers a question similar to:
Which running program does this execution belong to, and what memory and resources belong to it?
The thread answers a different question:
Where is this particular stream of execution currently up to?
And the processor answers the final question:
What machine instruction do I execute now?
Once those responsibilities are separated, the architecture becomes much easier to understand.
04 · TUTORIAL
A Process Can Exist With Several Streams of Execution
If the process itself were the thing that represented one stream of execution, we would immediately have a problem when an application became multithreaded.
Imagine our process contains three threads:
CODE EXAMPLE
MY APP PROCESS
Main Thread
Thread 2
Thread 3
Those three threads can be at completely different places in the application's machine code.
The main thread might currently be handling a user interaction.
Thread 2 might be processing an image.
Thread 3 might be executing work submitted by another subsystem.
One process therefore supports multiple independent execution contexts.
This is precisely why the distinction between a process and a thread exists.
05 · TUTORIAL
Each Thread Needs to Know Where It Is
In our previous articles we discussed the execution state associated with a thread.
Imagine the application's compiled machine code contains a sequence of instructions:
CODE EXAMPLE
Address 1000 Instruction A
Address 1004 Instruction B
Address 1008 Instruction C
Address 1012 Instruction D
Address 1016 Instruction E
Address 1020 Instruction F
Thread 1 might currently be executing around:
CODE EXAMPLE
Address 1012
while Thread 2 might be executing completely different instructions belonging to the same process.
EXECUTION DIAGRAM
THREAD 1
Current execution point
│
▼
Address 1012
THREAD 2
Current execution point
│
▼
Address 8920
Both execution contexts belong to the same application process.
But they are not at the same point in execution.
This is one of the reasons each thread requires its own execution state.
06 · TUTORIAL
The CPU Does Not Need to Understand Your Process as an App
This is where it helps to think one layer lower than Swift.
The processor does not understand that it is currently executing:
CODE EXAMPLE
RocketLaunchApp
It does not know that your application contains a profile screen, a network manager, a settings screen or a beautiful SwiftUI interface.
It does not understand your product.
It executes machine instructions.
At the hardware level, we are dealing with operations such as loading values, storing values, performing arithmetic, comparing values and branching through instructions.
Our higher-level software abstractions are enormously useful to us as developers, but eventually they have to become work that the processor architecture can execute.
07 · TUTORIAL
Follow One Thread All the Way Down
Imagine the user taps a button.
Our Swift code eventually causes some work to happen:
CODE EXAMPLE
func launchRocket() {
launchCount += 1
}
At the level where we write software, we think about:
EXECUTION DIAGRAM
launchRocket()
│
▼
launchCount += 1
But our application has already been compiled.
So when the program is actually running, processor execution ultimately concerns machine instructions.
EXECUTION DIAGRAM
Swift Source
│
▼
Compilation
│
▼
Machine Code
│
▼
Running Process
│
▼
Thread Execution Context
│
▼
OS Scheduling
│
▼
CPU Core
│
▼
Machine Instructions Execute
This is the chain we want to be able to draw without looking it up.
08 · TUTORIAL
So What Does the OS Scheduler Actually Schedule?
This question now becomes much easier to answer.
At the CPU scheduling level, the operating system schedules runnable threads onto available processor cores.
Imagine three applications are running.
CODE EXAMPLE
PHOTO APP PROCESS
Main Thread
Thread 2
MESSAGES APP PROCESS
Main Thread
Thread 2
Thread 3
OUR APP PROCESS
Main Thread
Thread 2
There are three processes, but there are seven threads in our simplified example.
Some of those threads may be waiting.
Some may be blocked.
Some may be runnable.
The scheduler's job is concerned with assigning runnable execution contexts to available CPU resources according to the operating system's scheduling policies.
So our simplified system-level picture becomes:
EXECUTION DIAGRAM
PROCESS A
├── Thread A1 ─────┐
└── Thread A2 │
│
PROCESS B │
├── Thread B1 ─────┤
├── Thread B2 ├──▶ OS SCHEDULER
└── Thread B3 │ │
│ ▼
PROCESS C │ CPU CORES
├── Thread C1 ─────┤
└── Thread C2 ─────┘
This diagram gives us a much more accurate understanding of operating-system multitasking.
09 · TUTORIAL
Then Why Do We Say the OS Switches Between Processes?
Because at a higher level, that description is useful.
If the CPU was executing a thread belonging to App A and then begins executing a thread belonging to App B, from the user's perspective the system has switched processor time between applications.
It is perfectly reasonable to describe that as switching between processes when explaining multitasking at a broad level.
But when we zoom into scheduling and concurrency, the more precise description is that the system schedules threads belonging to those processes.
So both statements can appear:
CODE EXAMPLE
HIGH-LEVEL DESCRIPTION
The OS switches between processes.
MORE PRECISE EXECUTION DESCRIPTION
The OS schedules runnable threads
belonging to those processes
onto CPU cores.
The second description is the one we need when learning concurrency.
10 · TUTORIAL
What Happens to the Process Memory?
This is where the process becomes essential again.
A thread does not exist as an independent little application with its own completely separate copy of all the process's state.
Threads belonging to the same process operate within the same process virtual address space and can access shared process memory.
Imagine:
EXECUTION DIAGRAM
┌─────────────────────────────────────────┐
│ MY APP PROCESS │
│ │
│ SHARED PROCESS MEMORY │
│ │
│ counter = 10 │
│ ▲ ▲ │
│ │ │ │
│ Main Thread Thread 2 │
│ │
└─────────────────────────────────────────┘
Both threads belong to the same process.
Both may be able to reach the same mutable value.
This is extremely useful.
It is also the source of one of the fundamental problems of multithreaded programming.
11 · TUTORIAL
The Threads Share Memory but Have Independent Execution
Read that sentence again because it captures the problem beautifully:
💡 Important Idea
Threads within a process can share access to process memory while maintaining independent execution contexts.
That combination is incredibly powerful.
Thread 1 can make progress.
Thread 2 can make progress.
Both can work with data belonging to the same application.
But now we have the possibility that both reach the same mutable state at overlapping times.
And suddenly:
CODE EXAMPLE
counter += 1
is no longer as innocent as it looks.
12 · TUTORIAL
Return to Our Lost Counter
Suppose:
CODE EXAMPLE
var counter = 10
Thread 1 executes work conceptually resembling:
EXECUTION DIAGRAM
READ counter → 10
ADD 1 → 11
WRITE → 11
Thread 2 wants to do exactly the same thing.
Because these are independent execution contexts, their operations can overlap or interleave.
EXECUTION DIAGRAM
THREAD 1 THREAD 2
READ 10
READ 10
ADD 1 → 11
ADD 1 → 11
WRITE 11
WRITE 11
Two increments were requested.
The final value can still be:
CODE EXAMPLE
11
Now look at this bug using our process/thread/processor model.
The process is not the problem.
The processor is not malfunctioning.
The operating system is not randomly corrupting our integer.
We created a program in which multiple execution contexts can access shared mutable state without sufficient synchronisation.
The machine is doing exactly what we allowed it to do.
13 · TUTORIAL
This Is Why the Process Must Stay in Our Diagram
It would therefore be a mistake to learn:
CODE EXAMPLE
The thread plugs into the CPU.
and then throw the process away.
The complete model needs both concepts.
EXECUTION DIAGRAM
┌─────────────────────────────────────────┐
│ APP PROCESS │
│ │
│ Shared Address Space │
│ Application State │
│ Code / Resources │
│ │
│ Thread 1 ───────────┐ │
│ Thread 2 │ │
│ Thread 3 │ │
│ │ │
└─────────────────────────────┼────────────┘
│
▼
OS SCHEDULER
│
▼
CPU CORE
The process tells us where the execution belongs and gives those threads their shared application environment.
The thread gives us the execution context.
The CPU performs the actual machine instructions.
14 · TUTORIAL
What Happens During a Context Switch?
Suppose the CPU is currently executing Thread 1 from our application.
EXECUTION DIAGRAM
MY APP PROCESS
│
▼
Thread 1
│
▼
CPU CORE
The operating system later decides another runnable thread should execute.
Enough of Thread 1's execution state is preserved so that it can continue correctly later.
The next thread's required execution state is established.
Now the processor can continue using another execution context:
EXECUTION DIAGRAM
ANOTHER PROCESS
│
▼
Thread 4
│
▼
CPU CORE
If the newly scheduled thread belongs to a different process, the execution environment associated with that process must also be respected, including the appropriate virtual-memory context.
This is one reason switching execution has a cost.
The CPU is not simply changing the name written above the current instruction.
Execution context matters.
15 · TUTORIAL
Now Add Multiple Processor Cores
Our single-core model has been deliberately useful because it allows us to see concurrency without parallelism.
Now imagine four cores.
EXECUTION DIAGRAM
MY APP PROCESS
Main Thread ─────────────▶ Core 1
Thread 2 ────────────────▶ Core 2
Thread 3 ────────────────▶ Core 3
ANOTHER APP PROCESS
Main Thread ─────────────▶ Core 4
Several threads can now genuinely execute machine instructions at the same time.
Notice what still has not happened.
We did not connect an entire process to Core 1.
We connected a runnable thread belonging to that process.
Another thread from the same process can simultaneously execute on another available core.
This is parallelism.
16 · TUTORIAL
A Process Can Therefore Use More Than One Core at Once
This follows naturally from the model.
If a process contains multiple runnable threads, the operating system can potentially schedule different threads belonging to that same process onto different processor cores.
EXECUTION DIAGRAM
┌──────────────────────────────────────┐
│ MY APP PROCESS │
│ │
│ Thread 1 ─────────────────▶ Core 1 │
│ Thread 2 ─────────────────▶ Core 2 │
│ Thread 3 ─────────────────▶ Core 3 │
│ │
└──────────────────────────────────────┘
This is another reason the phrase:
CODE EXAMPLE
The process runs on the processor.
is too coarse for serious concurrency reasoning.
Which part of the process?
Which thread?
Which core?
There may be several execution contexts belonging to the same process running simultaneously.
17 · TUTORIAL
Now Think About the Main Thread Again
In the previous article, we learned that our application's initial execution begins on the main thread.
We can now place that thread correctly in the complete architecture:
EXECUTION DIAGRAM
┌─────────────────────────────────────────┐
│ MY APP PROCESS │
│ │
│ Application Memory │
│ │
│ MAIN THREAD ─────────────┐ │
│ │ │
└──────────────────────────────────┼──────┘
│
▼
OS Scheduler
│
▼
CPU Core
│
▼
Machine Instructions
When we say:
CODE EXAMPLE
Don't block the main thread.
we can now translate it into something much more concrete.
Do not perform long-running synchronous work on the execution context responsible for important main-thread work, because while that thread is occupied executing those instructions, other work requiring that same execution context cannot make timely progress.
The phrase is no longer mysterious.
18 · TUTORIAL
Where Does Grand Central Dispatch Fit?
GCD sits above these underlying execution resources.
When we write:
CODE EXAMPLE
DispatchQueue.global().async {
processImage()
}
we are not plugging a dispatch queue into the processor.
A queue is not something a CPU core executes.
We are submitting work to GCD.
GCD coordinates the execution of that work using underlying worker threads.
Eventually:
EXECUTION DIAGRAM
DISPATCHED WORK
│
▼
GCD
│
▼
Worker Thread
│
▼
OS Scheduling
│
▼
CPU Core
Again, the thread is the execution context that ultimately reaches the processor.
19 · TUTORIAL
And Where Does Swift Concurrency Fit?
The same rule still applies.
When we create:
CODE EXAMPLE
Task {
await loadProfile()
}
the Task itself is not plugged into a CPU core.
A Task is a higher-level representation of asynchronous work.
Eligible synchronous portions of Swift concurrency work are arranged through executors and eventually execute using underlying system threads.
A useful simplified picture is:
EXECUTION DIAGRAM
SWIFT TASK
│
▼
Runnable Job
│
▼
Executor
│
▼
System Thread
│
▼
OS Scheduler
│
▼
CPU Core
│
▼
Machine Instructions
The hardware has not suddenly learned what a Swift Task is.
The processor still executes machine instructions through an execution context supplied by a system thread.
Swift Concurrency gives us a much better abstraction for deciding how application work should be organised above that machinery.
20 · TUTORIAL
This Is Why “Task Is Not a Thread” Matters
Developers are frequently told:
CODE EXAMPLE
A Task is not a thread.
Without understanding the underlying architecture, that sentence becomes another fact to memorise.
Now we can actually explain it.
A thread is an operating-system execution context that can be scheduled onto a processor core.
A Swift Task represents asynchronous work managed by the Swift concurrency system.
The Task may suspend.
It may later become eligible to continue.
Its work ultimately requires a thread when machine instructions actually need to execute, but the Task does not need to permanently own one particular thread for its entire lifetime.
That separation is one of the major advantages of the modern concurrency model.
21 · TUTORIAL
Three Layers — Three Different Responsibilities
If you remember only one diagram from this article, make it this one:
EXECUTION DIAGRAM
PROCESS
│
│ "Where does this running
│ application and memory belong?"
│
▼
THREAD
│
│ "What execution context
│ is making progress?"
│
▼
CPU CORE
│
│ "Which machine instruction
│ do I execute now?"
│
▼
MACHINE EXECUTION
Those are different layers.
Do not collapse them into one idea.
The process is not a thread.
The thread is not a processor.
A dispatch queue is not a thread.
A Swift Task is not a thread.
And none of those high-level abstractions changes the fundamental fact that processor cores ultimately execute machine instructions.
22 · TUTORIAL
Why This Matters to an iOS Developer
You may wonder whether knowing precisely what gets scheduled onto a CPU core actually changes the Swift you write tomorrow morning.
It does, because it removes ambiguity from almost every concurrency discussion that follows.
When somebody says:
CODE EXAMPLE
Move this onto another thread.
you understand what a thread actually represents.
When somebody says:
CODE EXAMPLE
Dispatch this to a background queue.
you understand that the queue is an abstraction for organising work and not the physical execution context itself.
When somebody says:
CODE EXAMPLE
Task doesn't create a thread.
you understand why it does not need to.
When somebody says:
CODE EXAMPLE
These two threads are racing
over the same value.
you can picture two independent execution contexts belonging to the same process, potentially reaching the same shared memory while their instructions are scheduled onto available CPU cores.
And when somebody says:
CODE EXAMPLE
Swift Concurrency is cooperative.
you now have the lower-level architecture onto which that new model must eventually map.
This is what understanding gives us that memorising terminology does not.
23 · TUTORIAL
What to Remember
💡 What to Remember
At the CPU scheduling level, it is a runnable thread's execution context that is scheduled onto a processor core — not an entire process as one indivisible unit.
The process provides the running application's environment, including its virtual address space and resources.
A process can contain multiple threads, and each thread represents an independent execution context within that process.
Threads belonging to the same process can access shared process memory, which is one reason concurrent access to mutable state requires coordination.
The operating system schedules runnable threads onto available processor cores.
On a multicore processor, several threads belonging to the same process may execute in parallel on different cores.
Grand Central Dispatch queues are not threads. GCD uses underlying worker threads to execute submitted work.
Swift Tasks are not threads either. Swift Concurrency manages asynchronous work at a higher level, while eligible work ultimately executes using system threads scheduled by the operating system.
The complete foundational model is:
EXECUTION DIAGRAM
Process
↓
Threads
↓
OS Scheduling
↓
CPU Cores
↓
Machine Instructions
24 · TUTORIAL
Your Next Move
We have now established exactly what reaches the processor.
The application lives inside a process.
The process contains threads.
Runnable threads are scheduled onto processor cores.
The processor executes machine instructions.
But our system-level diagram has introduced another question.
Imagine three applications:
EXECUTION DIAGRAM
APP A PROCESS
├── Main Thread
└── Thread 2
APP B PROCESS
├── Main Thread
├── Thread 2
└── Thread 3
APP C PROCESS
├── Main Thread
└── Thread 2
What happens to all of those threads?
Does iOS take every thread from every running application and place them into one enormous global collection?
Or do the threads remain conceptually and structurally associated with the process that owns them while the operating system schedules runnable threads across the entire system?
Understanding that distinction will complete our picture of how processes, threads and system-wide scheduling fit together.
That is the next article:
Are Threads Grouped Together Globally in the OS or Are They Confined Inside Each App?
