top of page

01 · INTRODUCTION

How Does an Actor Execute Multiple Tasks Safely?

An actor executes multiple Tasks safely by ensuring that only one actor-isolated job executes on that actor at a time.

If several Tasks all want to access the same actor, they are not allowed to execute actor-isolated code simultaneously. The actor has a serial executor, and that executor ensures that the actor's isolated work executes one piece at a time.

This is one of those ideas that can sound far more complicated than it really is because Swift Concurrency introduces words such as actor, isolation, job, executor, serial and reentrancy.

So rather than memorising those words, let us start with the problem we already understand.

We have some mutable state.

Several Tasks want to access it.

And we do not want two pieces of code changing that state at the same time.

Actors give Swift a language-level system for controlling that access.

🔑 Terminology — Serialise

To serialise work means to make pieces of work execute one after another rather than at the same time.

If A, B and C need to execute, serial execution can be pictured as:

EXECUTION DIAGRAM

A → B → C

rather than A, B and C all executing simultaneously.

02 · TUTORIAL

Start With the Simplest Possible Actor

Imagine we have a counter:

CODE EXAMPLE

actor Counter {

    var value = 0

    func increment() {
        value += 1
    }
}

Now imagine that three different Tasks all want to increment the counter.

CODE EXAMPLE

Task {
    await counter.increment()
}

Task {
    await counter.increment()
}

Task {
    await counter.increment()
}

Those Tasks can exist concurrently.

They can all reach the point where they want work to be performed by the Counter actor.

But Swift does not allow their actor-isolated work to execute on that actor simultaneously.

Instead, we can begin with this simple mental model:

EXECUTION DIAGRAM

Task A wants Counter ───┐
                       │
Task B wants Counter ───┼────▶ Counter Actor
                       │
Task C wants Counter ───┘
                              │
                              ▼
                       Serial Executor
                              │
                              ▼
                       ONE AT A TIME

That final sentence is the important part.

One at a time.

03 · TUTORIAL

What Does Serial Execution Actually Look Like?

If the actor methods contain no suspension points, we can initially picture the three pieces of actor work like this:

EXECUTION DIAGRAM

Task A's actor work

████████████


            Task B's actor work

            ████████████


                        Task C's actor work

                        ████████████

The actor-isolated execution does not overlap.

Task A's actor work executes.

Then another actor-isolated job can execute.

Then another.

This is what we mean when we say that an actor has a serial executor.

💡 If There Are No Suspension Points

If there are no suspension points inside the actor work, the simple mental model is that one actor-isolated job executes its synchronous work, finishes, and then another actor-isolated job gets an opportunity to execute.

04 · TUTORIAL

This Is More Than Locking a Stored Property

It is tempting to imagine that an actor simply places a lock around value.

CODE EXAMPLE

var value = 0

Then perhaps Swift unlocks the property whenever nobody is reading or writing it.

That is not the best mental model.

The actor establishes an isolation domain.

The mutable state belongs to that actor's isolation, and actor-isolated code executes according to the actor's serial execution rules.

So rather than imagining:

EXECUTION DIAGRAM

Stored Property
      │
      ▼
     LOCK
      │
      ▼
Read / Write

begin with:

EXECUTION DIAGRAM

             ACTOR

      ┌─────────────────┐
      │                 │
      │  Isolated State │
      │                 │
      │    value = 0    │
      │                 │
      └─────────────────┘
               ▲
               │
        Serial Executor
               ▲
               │
       one isolated job
          at a time

This is a broader architectural idea than placing a lock around one variable.

05 · TUTORIAL

Why Does Serial Execution Protect the Value?

Return to:

CODE EXAMPLE

value += 1

At the Swift source-code level this appears to be one operation.

But the processor ultimately needs lower-level instructions that perform operations equivalent to reading the current value, calculating a new value and writing the result.

Conceptually:

EXECUTION DIAGRAM

READ value
    │
    ▼
ADD 1
    │
    ▼
WRITE value

If two independent pieces of multithreaded code can perform those operations concurrently, they can become interwoven.

Imagine the value begins at zero:

EXECUTION DIAGRAM

Task A: READ value → 0

Task B: READ value → 0

Task A: ADD 1

Task B: ADD 1

Task A: WRITE value → 1

Task B: WRITE value → 1

We attempted to increment the counter twice.

But the final value is:

CODE EXAMPLE

1

We have lost an update.

06 · TUTORIAL

The Actor Prevents That Actor-Isolated Execution From Overlapping

Now place the state inside an actor and access it through actor-isolated code.

CODE EXAMPLE

actor Counter {

    var value = 0

    func increment() {
        value += 1
    }
}

The actor's serial execution means the two actor-isolated jobs do not execute simultaneously.

EXECUTION DIAGRAM

TASK A

READ value → 0
ADD 1
WRITE value → 1

──────── actor work finishes ────────

TASK B

READ value → 1
ADD 1
WRITE value → 2

Now we get:

CODE EXAMPLE

value = 2

The important point is not that Swift somehow made += into a magical operation.

The important point is that the actor prevented two actor-isolated jobs from executing against its isolated state simultaneously.

07 · TUTORIAL

This Should Feel Familiar to GCD Developers

If you have worked with Grand Central Dispatch, you may already be thinking that this sounds rather familiar.

Before actors, one approach to protecting mutable state was to create a serial dispatch queue and establish a rule that access to the protected state must go through that queue.

Conceptually:

CODE EXAMPLE

let queue = DispatchQueue(
    label: "com.example.counter"
)

queue.async {
    // access protected state
}

The intention was straightforward.

If everybody accesses the state through one serial queue, the protected operations can execute one after another rather than simultaneously.

So as an initial bridge from GCD into Swift Concurrency, it is reasonable to think:

💡 Familiar Mental Model

An actor can initially feel similar to placing a serial execution gateway in front of mutable state.

But actors take this idea much further.

08 · TUTORIAL

The Old Queue Rule Was Our Responsibility

Imagine we had designed a class containing shared state:

CODE EXAMPLE

final class Counter {

    var value = 0

}

Then our team created a serial queue and decided:

CODE EXAMPLE

IMPORTANT:

Every developer must remember
to access Counter through
our serial queue.

That can work as an architectural convention.

But the convention exists largely in the minds of the developers and in the structure of the code they have written.

Another developer can accidentally introduce access that does not follow the intended synchronization strategy.

The compiler does not automatically understand our informal rule simply because we created a dispatch queue.

09 · TUTORIAL

Actors Make Isolation Part of Swift

With an actor:

CODE EXAMPLE

actor Counter {

    private var value = 0

    func increment() {
        value += 1
    }
}

the isolation boundary is part of Swift's concurrency model.

The compiler understands actor isolation.

Swift can therefore diagnose code that attempts to cross actor isolation incorrectly.

That is a major conceptual improvement.

EXECUTION DIAGRAM

OLD APPROACH

Shared Mutable State
        │
        ▼
Developer-created synchronization
        │
        ▼
Team convention
        │
        ▼
"Everybody remember the rule."


ACTOR APPROACH

Actor-isolated Mutable State
        │
        ▼
Actor Isolation
        │
        ▼
Serial Executor
        │
        ▼
Compiler understands
the isolation boundary

We are moving responsibility away from an undocumented agreement between developers and into the language's concurrency system.

10 · TUTORIAL

So Do Entire Tasks Execute One After Another?

This is where our simple explanation needs one very important refinement.

If the actor work contains no suspension points, our original diagram is an excellent starting model:

EXECUTION DIAGRAM

Task A's actor work
██████████

          Task B's actor work
          ██████████

                    Task C's actor work
                    ██████████

But asynchronous actor methods can contain suspension points.

And once we introduce suspension, we must distinguish between an entire Task and an actor-isolated job that is currently executing.

11 · TUTORIAL

Now Add await

Imagine our actor method becomes asynchronous:

CODE EXAMPLE

actor Counter {

    var value = 0

    func update() async {

        value += 1

        await something()

        value += 1
    }
}

Task A enters the actor and begins executing the actor-isolated method.

EXECUTION DIAGRAM

TASK A

value += 1
    │
    ▼
await something()

If the asynchronous operation causes Task A to suspend, Task A has not finished.

There is still more code to execute:

CODE EXAMPLE

value += 1

But Task A is temporarily suspended.

This changes the picture.

12 · TUTORIAL

The Actor Does Not Remain Locked to Task A

One possible design would have been for Task A to claim the actor when it first enters:

EXECUTION DIAGRAM

Task A enters actor

████████
       │
       ▼
     await

       │
       │
       │ ACTOR LOCKED
       │ TO TASK A
       │
       │
       ▼

Task A resumes

████████

But that would mean the actor could sit unable to process other eligible work while Task A waits for an asynchronous operation that might take a considerable amount of time.

That would work against the cooperative model we have been developing.

Instead, when Task A actually suspends, another eligible actor-isolated job can get an opportunity to execute.

13 · TUTORIAL

Another Task Can Now Get a Turn

Our execution can therefore look like this:

EXECUTION DIAGRAM

Task A — actor-isolated job

██████████
         │
         ▼
       await
         │
         ▼
      SUSPEND


          Task B — actor-isolated job

          ██████████


                    Task C — actor-isolated job

                    ██████████


                              Task A — continuation

                              ██████████

Look carefully at what has happened.

Task A started first.

Task A has still not finished.

But while Task A was suspended, Task B was able to perform actor-isolated work.

Task C was also able to perform actor-isolated work.

Then Task A eventually became eligible to continue.

And throughout the entire sequence, only one actor-isolated job was executing at any particular moment.

14 · TUTORIAL

This Is the Important Distinction

💡 Jobs Are Serialised — Entire Async Tasks Are Not Necessarily Completed One After Another

An actor's serial executor ensures that actor-isolated jobs execute one at a time.

But an asynchronous Task can suspend before its entire operation has finished.

When that happens, another eligible actor-isolated job may execute before the original Task continues.

This distinction is essential.

Otherwise we might incorrectly imagine:

EXECUTION DIAGRAM

Task A starts
     │
     ▼
Task A completely finishes
     │
     ▼
Task B starts
     │
     ▼
Task B completely finishes
     │
     ▼
Task C starts

That simple model works for uninterrupted synchronous actor work.

It does not describe the entire lifetime of asynchronous Tasks that can suspend.

15 · TUTORIAL

A Task Can Therefore Be Split Across Several Turns

Task A may effectively receive more than one period of actor-isolated execution.

EXECUTION DIAGRAM

TASK A

Actor Job A1
████████
       │
       ▼
    SUSPEND

       ⋮

       ⋮

Actor Job A2
                          ████████

During the gap, other jobs can execute:

EXECUTION DIAGRAM

ACTOR'S SERIAL EXECUTION

Task A — Job A1
████████
       │
       ▼
    suspend

        Task B — Job B
        ████████

                Task C — Job C
                ████████

                        Task A — Job A2
                        ████████

Every block is serial.

There is never actor-isolated execution from A and B overlapping.

There is never actor-isolated execution from B and C overlapping.

But Task A's complete lifetime stretches across several turns.

16 · TUTORIAL

🔑 Terminology — Reentrancy

🔑 Terminology — Reentrancy

An actor method can begin executing, suspend at an await, allow other eligible actor-isolated work to execute, and later continue.

This behaviour is called actor reentrancy.

The word sounds complicated.

The picture is not:

EXECUTION DIAGRAM

Task A
████████
       │
       ▼
    SUSPEND

        Task B
        ████████

                Task A
                ████████

Task A entered.

Task A suspended.

Task B got a turn.

Task A came back later.

That is the basic idea we need before studying all of the consequences.

17 · TUTORIAL

But Something Important May Have Changed

Reentrancy gives our application the ability to continue making progress while one actor operation is suspended.

But it introduces something we need to remember when writing actor methods.

The actor's state may have legitimately changed while our Task was suspended.

Consider:

CODE EXAMPLE

actor BankAccount {

    var balance = 100

    func inspectBalance() async {

        print(balance)

        await something()

        print(balance)
    }
}

Imagine the first print outputs:

CODE EXAMPLE

100

Then the Task suspends.

While it is suspended, another Task may execute actor-isolated code that changes the balance.

EXECUTION DIAGRAM

TASK A

print(balance)  // 100

      │
      ▼
    await

      │
      ▼
   SUSPEND


TASK B

balance -= 20


TASK A RETURNS

print(balance)  // do not assume 100

There has been no simultaneous unsafe access to balance.

The actor's serial executor still did its job.

But time passed.

Another actor-isolated job legitimately executed.

The state changed.

18 · TUTORIAL

Data-Race Safety Does Not Mean State Never Changes

This is an extremely important distinction.

The actor protects us from overlapping actor-isolated execution.

It does not promise:

CODE EXAMPLE

Once Task A enters this actor,
all actor state is frozen until
Task A completely finishes forever.

If Task A suspends, other eligible work can execute.

That work may change the actor's state.

So when Task A returns from an await, it should not blindly assume that every piece of actor state still has exactly the value it had before suspension.

19 · TUTORIAL

This Connects Back to Our Previous Article

We previously learned that suspension is not simply another word for blocking.

A suspended Task can remain unfinished without continuously occupying the execution resource it was previously using.

Actors benefit from exactly that model.

EXECUTION DIAGRAM

Task A
████████
       │
       ▼
    suspend


Task B
        ████████


Task C
                ████████


Task A continuation
                        ████████

Instead of Task A monopolising the actor throughout an asynchronous wait, other eligible work can make progress.

That is cooperative concurrency appearing again.

20 · TUTORIAL

Think About Our Animation Frames Again

Throughout this series we have been building a different way of thinking about application performance.

The goal is not:

CODE EXAMPLE

One operation starts.

Let it consume everything
until absolutely all of its
work has finished.

The goal is to allow the important pieces of our application to continue making progress together.

An actor follows that philosophy.

It protects isolated state by preventing overlapping actor-isolated execution.

But if one Task suspends, the actor does not need to sit idle merely because that Task has not completed its entire asynchronous operation.

Another eligible job can make progress.

Later, the original Task can continue.

21 · TUTORIAL

The Actor Is Not Simply a Lock

We can now see why saying:

CODE EXAMPLE

An actor locks its properties.

throws away too much useful information.

A better mental model is:

EXECUTION DIAGRAM

Multiple Tasks
      │
      ▼
Actor Isolation Boundary
      │
      ▼
Serial Executor
      │
      ▼
One actor-isolated job
executing at a time
      │
      ▼
Protected Actor State

And when one Task suspends:

EXECUTION DIAGRAM

Task A Job
████████
       │
       ▼
    suspend

        Task B Job
        ████████

                Task C Job
                ████████

                        Task A Continuation
                        ████████

The actor remains serial.

The Tasks remain concurrent.

Those two statements can both be true.

22 · TUTORIAL

The Actor Is Serial While the Application Is Concurrent

This is perhaps the most beautiful part of the model.

We can have hundreds or thousands of asynchronous Tasks existing throughout an application.

Those Tasks can make progress concurrently.

But when several of them need access to one particular actor's isolated state, that actor provides a controlled serial execution domain.

EXECUTION DIAGRAM

CONCURRENT APPLICATION

Task A ────────────────┐
                       │
Task B ────────────────┤
                       │
Task C ────────────────┼────▶ ACTOR
                       │        │
Task D ────────────────┤        ▼
                       │   Serial Execution
Task E ────────────────┘        │
                                ▼
                         Isolated State

Concurrency therefore does not mean everything should execute simultaneously.

Good concurrent architecture is about deciding what may progress concurrently and what must be isolated from simultaneous execution.

23 · TUTORIAL

This Is What Swift Is Solving for Us

For years, iOS developers have had to build synchronization systems ourselves.

We used serial queues.

We used locks.

We created conventions around which queue owned which state.

We wrote comments warning other developers not to access certain properties from the wrong place.

And when those conventions were broken, the resulting bugs could be intermittent and extremely difficult to reproduce.

Actors move an important part of that problem into Swift's concurrency model.

We describe an isolation boundary.

Swift understands that boundary.

The actor's executor serialises actor-isolated jobs.

The compiler can enforce rules around crossing the isolation boundary.

And asynchronous Tasks can still suspend and allow other work to make progress.

This is not merely a faster way to write the serial queues we already had.

It is a more structured way to describe ownership, isolation and concurrent execution.

24 · TUTORIAL

What to Remember

💡 What to Remember

An actor executes multiple Tasks safely by ensuring that only one actor-isolated job executes on that actor at a time.

To serialise work means to make pieces of work execute one after another rather than simultaneously.

An actor has a serial executor.

Multiple Tasks can concurrently request work from the same actor, but their actor-isolated execution does not overlap.

If the actor work contains no suspension points, the simple mental model is that one actor-isolated job executes its synchronous work, finishes, and another job then gets an opportunity to execute.

The actor is not best understood as merely placing a lock around one stored property. The actor establishes an isolation domain for its state and actor-isolated operations.

Actors can feel conceptually similar to protecting shared mutable state with a serial GCD queue, but actor isolation is understood and enforced by Swift's concurrency system.

An asynchronous actor method can suspend at an await.

When it suspends, the actor does not remain exclusively reserved for that entire Task until the Task eventually finishes.

Another eligible actor-isolated job may execute while the original Task is suspended.

The original Task can later continue as another actor-isolated job.

This means actor-isolated jobs execute serially, while entire asynchronous Tasks do not necessarily begin and completely finish one after another.

This behaviour is called actor reentrancy.

Because other actor work may execute during a suspension, actor state may legitimately change across an await.

The actor remains data-race safe because actor-isolated jobs are not executing simultaneously, but developers must still reason about logical state changes across suspension points.

The mental model is:

EXECUTION DIAGRAM

Many concurrent Tasks
          ↓
     Same Actor
          ↓
   Serial Executor
          ↓
One actor-isolated job
    at a time
          ↓
     Safe State

And if a Task suspends:

Task A Job
    ↓
  await
    ↓
 suspend
    ↓
Task B Job
    ↓
Task C Job
    ↓
Task A continues

25 · TUTORIAL

Your Next Move

We now know how an actor can sit inside a highly concurrent application while still protecting its own mutable state.

The application can contain many Tasks.

Those Tasks can make progress concurrently.

But when they enter one actor's isolation domain, the actor's serial executor ensures that actor-isolated jobs execute one at a time.

And when an asynchronous actor operation suspends, another job can get a turn without introducing simultaneous access to the actor's isolated state.

That leaves us with an important word that we have now used repeatedly:

isolation.

What exactly is being isolated?

Is the actor isolating a thread?

Is it isolating stored properties?

Is it isolating functions?

What does it mean when the compiler tells us that something is actor-isolated?

And what exactly are we crossing when Swift tells us that code is crossing an isolation boundary?

That is the next article:

What Is Actor Isolation in Swift?

bottom of page