top of page

01 · INTRODUCTION

What Is Concurrency?

The short answer

Concurrency is a way of structuring a program so multiple tasks can make progress during the same period of time. Swift Concurrency provides the runtime, tasks and suspension model that make that coordination explicit.

Swift Concurrency gives Swift a new runtime for scheduling and managing asynchronous tasks.

CODE EXAMPLE

Task {
    let sortedNames = await sortNames(names)
    let sortedScores = await sortScores(scores)
    let sortedAges = await sortAges(ages)
}

💡 Important Terminology

Asynchronous = work that can suspend while waiting and resume later.

Concurrent = switching between tasks so they progress together.

Parallel = multiple pieces of work are literally executing at the same time.

02 · TUTORIAL

How It Works

In computer programming, concurrency means structuring execution so that multiple pieces of work can make progress during the same period of time.

The easiest way to understand this is to imagine a single-core processor running several threads. Because there is only one core, only one of those threads can actually execute at any instant. The operating system repeatedly gives each runnable thread an opportunity to make progress.

EXECUTION DIAGRAM

                   SWIFT CONCURRENCY RUNTIME - MAIN THREAD
                       │
                       │

Task 1     █████████████████████                  ███████████                 ███████                            ███████

Task 2                           ███████                      ███████                 █████████████████                  █████████

Task 3                                   █████████                    ███████                           █████████                  ████████████    

             ───────────────────────────────────────────────────────►
                                TIME

Look familiar?

This is the same basic idea that made multitasking possible on single-core computers: several pieces of work can all make progress without all of them literally executing at the same instant.

Swift Concurrency brings a similar cooperative way of thinking into the structure of our own application. Instead of treating every operation as one uninterrupted stream of code that must finish immediately, we can describe work as tasks and give those tasks places where they are allowed to suspend.

CODE EXAMPLE

Task {
    let sortedNames = await sortNames(names)
    let sortedScores = await sortScores(scores)
    let sortedAges = await sortAges(ages)
}

A Task describes a unit of asynchronous work that the Swift Concurrency runtime can manage and schedule.

When that task reaches a suspension point and actually needs to wait, it can stop executing temporarily instead of occupying the thread while nothing useful can be done.

If the task was running on the MainActor, that suspension can give the main thread an opportunity to perform other eligible work required by the application and its user interface.

💡 Important Ideas

1. We created a Task, giving the Swift Concurrency runtime a unit of asynchronous work that it can manage and schedule.

2. We used await to introduce possible suspension points where that task may step aside and allow other eligible work to make progress.


Let's look more closely at the second idea: suspension.

CODE EXAMPLE

Task {

    let sortedNames = await sortNames(names)
                      // ↑ possible suspension point

    let sortedScores = await sortScores(scores)
                       // ↑ possible suspension point

    let sortedAges = await sortAges(ages)
                     // ↑ possible suspension point
}

Each await marks a possible suspension point. If the asynchronous operation needs to wait, the current task can suspend and the Swift Concurrency runtime gets an opportunity to schedule other eligible work.

If the asynchronous operation actually needs to wait, the task can suspend instead of occupying the thread until the result becomes available.

That gives the Swift Concurrency runtime an opportunity to schedule other eligible work.

EXECUTION DIAGRAM

One Task

sortNames()
    │
    ▼
  await ──────► task may suspend
    │
    ▼
sortScores()
    │
    ▼
  await ──────► task may suspend
    │
    ▼
sortAges()
    │
    ▼
  await ──────► task may suspend

So the new way of thinking begins with two ideas:

Create Tasks to describe work.

Create suspension opportunities so those Tasks can cooperate with the scheduler.

This is the first important change in thinking.


Before Swift Concurrency, an iOS developer who was worried about occupying the main thread for too long would often begin by asking which background queue or additional thread should perform the work instead.

CODE EXAMPLE

DispatchQueue.global().async {
    let sortedNames = names.sorted()

    DispatchQueue.main.async {
        self.names = sortedNames
    }
}

Swift Concurrency gives us another option.

CODE EXAMPLE

Task {
    let sortedNames = await sortNames(names)
    self.names = sortedNames
}

We can structure our application as tasks that are able to suspend and later resume, allowing the runtime to schedule other eligible work whenever the current task gives it an opportunity.

Swift Concurrency manages the scheduling of tasks. Our code must cooperate by providing places where those tasks can suspend, yield or finish.

That idea is easier to understand if we first look at a scheduler we already use every day: the operating system.

03 · TUTORIAL

Operating Systems Already Schedule Our Threads

A thread is a stream of instructions that the operating system can schedule for execution on a processor core.

Every iOS application starts with a main thread, and additional threads can be used when an application needs other streams of execution.

We can loosely imagine each thread as containing a sequence of code that is ready to execute.

CODE EXAMPLE

func encodeVideo(_ frames: [VideoFrame]) {
    for frame in frames {
        encode(frame)
    }
}

A video-encoding application, for example, may use additional threads for expensive encoding work. Once a worker thread begins a long section of synchronous computation, that thread remains occupied by that computation until the work finishes, blocks, or otherwise gives up execution.

That may be perfectly reasonable for a worker thread.

Doing the same thing on the main thread is different.

CODE EXAMPLE

@MainActor
func prepareVideos() {
    for video in videos {
        encode(video)
    }

    status = "Finished"
}

The main thread is also required for important user-interface work. If our own code keeps it occupied for too long, UI work cannot run when it needs to.

This is why a long calculation on the main thread can make scrolling become jerky, delay an animation or make the application appear frozen.

The operating system itself faces a much larger version of the same scheduling problem.

At any moment there may be many runnable threads belonging to many applications and system services, but the processor only has a limited number of cores on which those threads can actually execute.

The operating system therefore schedules them.

EXECUTION DIAGRAM

                    CPU TIME
                       │
                       │

Thread 1     ███████             ███████             ███████

Thread 2             ███████             ███████             ███████

Thread 3                     ███████             ███████             ███████

             ───────────────────────────────────────────────────────►
                                TIME

On a simplified single-core processor, we can imagine Thread 1 receiving some execution time, followed by Thread 2, followed by Thread 3, before execution eventually returns to Thread 1.

The processor is not literally executing all three instruction streams simultaneously.

The operating system is repeatedly deciding which runnable thread gets an opportunity to execute.

This happens so quickly that all three threads appear to be making progress together.

That is the basic idea of concurrency at the operating-system level.

It is also the mental model that makes Swift Concurrency much easier to understand.

04 · TUTORIAL

Swift Concurrency Introduces Another Scheduling Layer

The operating system schedules threads.

Swift Concurrency gives our application a higher-level system for scheduling tasks.

EXECUTION DIAGRAM

Our Swift Code
      │
      ▼
    Tasks
      │
      ▼
Swift Concurrency Runtime
      │
      ▼
  Executors
      │
      ▼
   Threads
      │
      ▼
OS Thread Scheduler
      │
      ▼
   CPU Cores

This does not replace threads.

Threads still exist underneath Swift Concurrency, and the operating system still decides when those threads receive processor time.

What changes is the level at which we normally describe our application's work.

CODE EXAMPLE

Task {
    let names = await sortNames(names)
    let scores = await sortScores(scores)
    let ages = await sortAges(ages)
}

Instead of immediately saying:

Run this function on another thread.

we can begin by saying:

This is a task my application needs to perform.

The runtime can then schedule eligible task work onto executors, which ultimately run that work using threads.

That sounds like a small distinction, but it changes the architecture of asynchronous Swift considerably.

05 · TUTORIAL

What Does This Have to Do With the Main Run Loop?

Our application's main thread repeatedly becomes available to perform different kinds of work.

It responds to events, timers and callbacks, runs our main-actor code, and participates in the work required to update the user interface.

For teaching purposes, we can draw a deliberately simplified picture of one opportunity for the main thread to do work.

CODE EXAMPLE

@MainActor
func handleRefresh() {
    updateApplicationState()
    updateVisibleState()
}

EXECUTION DIAGRAM

Main-thread execution opportunity

┌──────────────────────────────────────┐
│ Our application work                │
│ ███████████████████                  │
│                                      │
│ UI-related work                     │
│                         ████████     │
└──────────────────────────────────────┘

This diagram is a mental model, not a literal implementation of the iOS run loop.

SwiftUI does not simply append a render operation after the final line of our code on every loop iteration.

The useful idea is that the main thread must repeatedly become available so the system can process UI-related work at the times it is needed.

If our application work finishes quickly, there is plenty of opportunity for that to happen.

EXECUTION DIAGRAM

TIME ─────────────────────────────────────────────────────────────►

Opportunity 1           Opportunity 2           Opportunity 3

┌───────────────┐       ┌───────────────┐       ┌───────────────┐
│ Our Code      │       │ Our Code      │       │ Our Code      │
│ ██████        │       │ ████          │       │ █████         │
│               │       │               │       │               │
│ UI Work ███   │       │ UI Work ███   │       │ UI Work ███   │
└───────────────┘       └───────────────┘       └───────────────┘

If one section of synchronous application code takes much longer, UI work can be delayed.

EXECUTION DIAGRAM

┌─────────────────────────────────────────────────────────────┐
│ Our Code                                                    │
│ ███████████████████████████████████████████████████         │
│                                                             │
│ UI Work                                             ███     │
└─────────────────────────────────────────────────────────────┘
                                                    ▲
                                                    │
                                                 delayed

Nothing special happens when a particular amount of time is exceeded.

Swift Concurrency does not contain a hidden 60 FPS timer that stops our code when a frame budget has been consumed.

The consequence is simply that the main thread was unavailable when some UI work needed it.

On a 60 Hz display, a new display refresh occurs roughly every 16.7 milliseconds. We should not treat all of those 16.7 milliseconds as our private computation budget, but the number gives us a useful sense of how little uninterrupted main-thread work may be required before delays become visible.

06 · TUTORIAL

An Imaginary Task Manager

We can make the purpose of Swift Concurrency easier to reason about by writing a tiny task manager of our own.

CODE EXAMPLE

while tasksRemain {
    let task = nextTask()

    await task()
    await Task.yield()
}

The following class is not the implementation of Swift Concurrency.

It is a teaching model that lets us imagine why a runtime capable of managing tasks is useful.

CODE EXAMPLE

import Foundation

final class CooperativeTaskManager {

    typealias Work = () async -> Void

    private var tasks: [Work] = []

    func addTask(_ work: @escaping Work) {
        tasks.append(work)
    }

    func run() async {
        while !tasks.isEmpty {

            let nextTask = tasks.removeFirst()

            await nextTask()

            // Give other eligible work an opportunity
            // to execute before we continue.
            await Task.yield()
        }
    }
}

The class contains a queue of work.

Each time through run(), it removes one piece of work, executes it and then deliberately yields before taking another piece from the queue.

We could give it our three sorting jobs.

CODE EXAMPLE

let manager = CooperativeTaskManager()

manager.addTask {
    print(names.sorted())
}

manager.addTask {
    print(scores.sorted())
}

manager.addTask {
    print(ages.sorted())
}

Task {
    await manager.run()
}

Our imaginary manager now has three separate jobs to process.

EXECUTION DIAGRAM

Queued Work

┌─────────────────┐
│ Sort Names      │
├─────────────────┤
│ Sort Scores     │
├─────────────────┤
│ Sort Ages       │
└─────────────────┘
         │
         ▼
    Run one job
         │
         ▼
       Yield
         │
         ▼
other eligible work
may get an opportunity
         │
         ▼
    Run next job

This gives us a useful new thought process.

Instead of one enormous function containing everything our application needs to do, we can identify meaningful pieces of work that a scheduler understands individually.

If one task finishes, the scheduler can move on to another.

If a task suspends, another eligible task can potentially make progress while the first one waits.

But our imaginary manager also exposes the most important limitation of this entire model.

07 · TUTORIAL

The Scheduler Cannot Save Us From a Bad Task

Suppose the first job in our queue unexpectedly performs an enormous synchronous sort.

CODE EXAMPLE

manager.addTask {
    let result = enormousArray.sorted()
    print(result)
}

The manager calls that task.

Then the synchronous sorting code begins.

EXECUTION DIAGRAM

Task Manager
     │
     ▼
Start Task
     │
     ▼
██████████████████████████████████████████████████
             enormousArray.sorted()
██████████████████████████████████████████████████
     │
     ▼
task returns
     │
     ▼
Task Manager regains control

CODE EXAMPLE

func sortEverything() {
    let names = enormousNamesArray.sorted()
    let scores = enormousScoresArray.sorted()
    let ages = enormousAgesArray.sorted()

    // No cooperative boundary exists here.
}

The manager cannot check its queue halfway through Array.sorted().

It cannot decide that the sort has taken long enough and automatically pause it after the next five comparisons.

It cannot insert a UI update in the middle of arbitrary synchronous Swift code.

It only regains control when the work returns or reaches a point where it can suspend.

This is the crucial limitation that explains the word cooperative.

CODE EXAMPLE

func processLargeArray(_ values: [Int]) async {
    for (index, value) in values.enumerated() {
        process(value)

        if index.isMultiple(of: 500) {
            await Task.yield()
        }
    }
}

08 · TUTORIAL

Swift Concurrency Is Cooperative

The operating system can preempt a thread.

Swift's asynchronous task model does not arbitrarily preempt our synchronous Swift code between individual statements.

Possible suspension points are visible in asynchronous code.

This is why await matters.

Let's return to our sorting example and deliberately make each operation asynchronous.

CODE EXAMPLE

func sortNames(_ names: [String]) async -> [String] {
    await Task.yield()
    return names.sorted()
}

func sortScores(_ scores: [Int]) async -> [Int] {
    await Task.yield()
    return scores.sorted()
}

func sortAges(_ ages: [Int]) async -> [Int] {
    await Task.yield()
    return ages.sorted()
}

Task.yield() is useful here because it makes the cooperation visible.

We are not suggesting that real applications should put Task.yield() inside every small sorting function.

The arrays in this example are tiny. Their synchronous sorting work is so small that introducing concurrency would normally be unnecessary.

We are using Task.yield() to expose the scheduling mechanism.

Now our task can call each operation using await.

CODE EXAMPLE

Task {
    let sortedNames = await sortNames(names)
    let sortedScores = await sortScores(scores)
    let sortedAges = await sortAges(ages)
}

Follow only the first call.

CODE EXAMPLE

let sortedNames = await sortNames(names)

The task enters sortNames() and eventually reaches:

CODE EXAMPLE

await Task.yield()

At this point the task is voluntarily giving the runtime an opportunity to schedule other eligible work.

I can step aside here. If something else is ready to run, this is an opportunity to let it progress.

EXECUTION DIAGRAM

Our Task

sortNames()
    │
    ▼
Task.yield()
    │
    ├──────────────► Other eligible work may run
    │
    ◄─────────────── Our task is scheduled again
    │
    ▼
names.sorted()
    │
    ▼
return result

Eventually our task continues after the yield.

It performs the synchronous sort and returns its result.

Later it reaches sortScores(), where another opportunity to yield exists, and then sortAges().

Notice what has changed.

We still have synchronous work.

names.sorted() still executes synchronously once it starts.

Swift Concurrency has not transformed the implementation of Array.sorted() into hundreds of tiny interruptible operations.

What we have done is structure our larger operation so that there are meaningful places where the task can cooperate with the scheduler.

09 · TUTORIAL

await Does Not Mean "Render Another Frame"

There is an attractive mental picture we need to resist.

Looking at three await expressions, we might imagine that each one guarantees another execution of the main run loop and another rendered UI frame.

CODE EXAMPLE

let sortedNames = await sortNames(names)    // frame 1?
let sortedScores = await sortScores(scores) // frame 2?
let sortedAges = await sortAges(ages)       // frame 3?

Swift makes no such guarantee.

await marks a possible suspension point.

If the asynchronous operation needs to wait, the current task can suspend.

CODE EXAMPLE

func loadLaunches() async throws -> [Launch] {
    let (data, _) = try await URLSession.shared.data(from: launchesURL)
    return try JSONDecoder().decode([Launch].self, from: data)
}

If suspension occurs, the thread that was executing the task does not need to remain occupied simply waiting for the result.

That thread can become available for other eligible work.

EXECUTION DIAGRAM

MainActor Task

████████ await
         │
         ▼
      suspends

Main Thread
         █████████████████
         other eligible work

                          │
                          ▼
                       resumes

                          ███████

If this is main-actor work, that opportunity can matter enormously.

The main thread may now be able to process other main-actor work needed by the application and its user interface.

But suspension is not synonymous with rendering.

An await does not mean:

Render the next frame now.

It means:

This asynchronous operation may need to wait, and this task is allowed to suspend here while it does.

10 · TUTORIAL

Why Not Just Create More Threads?

We could still move expensive work away from the main thread.

There are situations where CPU-intensive work genuinely should execute concurrently away from the main actor.

But creating a dedicated thread for every operation would miss one of the main architectural benefits of Swift Concurrency.

Threads are comparatively expensive operating-system resources.

Tasks are higher-level units of work that the runtime can schedule over a much smaller collection of underlying threads.

One thousand Swift tasks do not imply one thousand threads.

CODE EXAMPLE

for url in imageURLs {
    Task {
        await downloadImage(from: url)
    }
}

This changes the question we ask when designing an application.

Instead of assuming that every operation which might become expensive deserves its own thread, we can first ask whether the operation should be represented as asynchronous work, where it can suspend, whether it belongs on the main actor, and whether genuinely CPU-intensive work should be allowed to execute concurrently elsewhere.

11 · TUTORIAL

UI Code and Model Work

This distinction becomes particularly useful when thinking about application architecture.

User-interface state usually belongs on the main actor.

CODE EXAMPLE

@MainActor
final class LaunchViewModel {
    var launches: [Launch] = []

    func refresh() async throws {
        launches = try await launchService.fetchLaunches()
    }
}

That keeps UI-related state serialized and associated with the main thread.

Networking, decoding, file processing, database work and expensive calculations do not automatically need to occupy the main actor.

EXECUTION DIAGRAM

Model / Service Work
        │
        ▼
Swift Concurrency
concurrent / asynchronous work
        │
        ▼
      Result
        │
        ▼
    MainActor
        │
        ▼
UI State / SwiftUI / UIKit

This does not mean Swift Concurrency is only for model code.

UI code uses Task, async, await, cancellation and actors too.

The more useful distinction is:

UI state belongs on the MainActor. Work that does not require the MainActor should not unnecessarily occupy it.

12 · TUTORIAL

A New Way to Think About Swift Code

For years, iOS developers learned to ask:

Could this function take a long time? If so, which background queue should I put it on?

That question has not disappeared.

Long-running CPU work still needs careful execution away from the main actor when appropriate.

But Swift Concurrency gives us a better set of questions to ask before we start manually thinking about threads.

What are the meaningful pieces of work in this operation?

Which pieces depend on one another?

Where can this task naturally suspend?

How long does this task execute synchronously before it gives the scheduler another opportunity?

Does this work actually need the MainActor?

This is the architectural shift.

We create tasks representing meaningful work.

We allow asynchronous operations to suspend when they need to wait.

We avoid unnecessarily long synchronous regions on execution resources that other work needs.

And we allow the Swift Concurrency runtime to manage the scheduling of eligible task work instead of manually orchestrating every thread ourselves.

EXECUTION DIAGRAM

Developer
    │
    │ describes work
    ▼
  Tasks
    │
    │ cooperate through
    │ suspension / yielding / completion
    ▼
Swift Concurrency Runtime
    │
    │ schedules eligible work
    ▼
 Executors
    │
    ▼
 Threads
    │
    ▼
OS Scheduler
    │
    ▼
CPU Cores

This is why describing Swift Concurrency simply as a way of making several things happen at the same time can be misleading.

Swift Concurrency can certainly participate in parallel execution when independent work is allowed to execute concurrently and hardware resources are available.

But parallelism is not the best place to begin understanding the system.

A better starting point is this:

Swift Concurrency gives our application a runtime for managing asynchronous tasks. The runtime schedules eligible work, while our code cooperates by providing meaningful task boundaries and places where execution can suspend.

Once we understand that relationship, Task, async and await stop looking like unrelated new Swift keywords.

They become parts of a scheduling model.

And that is where Swift Concurrency begins.

bottom of page