01 · INTRODUCTION
What Is Cooperative Scheduling in Swift?
The short answer
Cooperative scheduling is the execution model in which Swift tasks make room for other eligible work by suspending or completing their current synchronous job.
The runtime can choose among jobs that are ready to execute. It does not normally interrupt arbitrary synchronous Swift code merely because another task is waiting for the same executor.
EXECUTION DIAGRAM
RUNNING JOB
│
├── reaches a suspension point
│
└── or completes
│
▼
EXECUTOR CAN RUN ANOTHER ELIGIBLE JOB
The task cooperates by returning the execution opportunity when its current work no longer needs it.
💡 Important Terminology
Job = a synchronous portion of task work that an executor can run.
Eligible = ready to execute now rather than suspended or waiting on a dependency.
Executor = a service that accepts eligible jobs and arranges their execution.
Cooperation = running code reaches a point where the executor can make progress on another job.
02 · TUTORIAL
From Tasks to Runnable Jobs
A task represents the complete lifetime of an asynchronous operation.
CODE EXAMPLE
Task {
status = "Loading"
let launches = try await api.loadLaunches()
status = "Finished"
}
The task may not execute as one uninterrupted block.
EXECUTION DIAGRAM
ONE TASK
JOB 1
status = "Loading"
begin loadLaunches()
│
▼
SUSPEND
│
▼
JOB 2
receive launches
status = "Finished"
Each job contains synchronous instructions that are currently eligible to run.
The suspension between them remains part of the task's lifetime, but there is no runnable task work during that wait.
03 · TUTORIAL
The Executor Chooses Among Eligible Jobs
Imagine three tasks associated with the main actor:
CODE EXAMPLE
Task { @MainActor in
await refreshLaunches()
}
Task { @MainActor in
updateCountdown()
}
Task { @MainActor in
showConnectionStatus()
}
Their main-actor jobs must take turns.
EXECUTION DIAGRAM
MAIN-ACTOR EXECUTOR — CONCEPTUAL MODEL
Eligible jobs
[Refresh job] [Countdown job] [Status job]
│
▼
Executor selects one eligible job
│
▼
Main execution domain runs its instructions
The executor provides serial execution for main-actor-isolated work.
It decides which eligible job is arranged next. Developers should not treat the diagram as a guaranteed first-in, first-out queue or rely on one exact scheduling order.
04 · TUTORIAL
Why Is the Scheduling Cooperative?
Once a synchronous job begins, its instructions run until the job reaches a suitable boundary.
CODE EXAMPLE
@MainActor
func refreshLaunches() async throws {
status = "Loading"
requestCount += 1
launches = try await api.loadLaunches()
status = "Finished"
}
The first two state changes execute synchronously.
When the task reaches the asynchronous call and genuinely suspends, the current job ends. The main-actor executor can arrange another eligible job.
EXECUTION DIAGRAM
REFRESH TASK
JOB 1 RUNS
status = "Loading"
requestCount += 1
│
▼
await
│
▼
TASK SUSPENDS
│
▼
ANOTHER ELIGIBLE JOB CAN RUN
The runtime did not pre-empt the code between those statements.
The task reached a point where it could no longer make immediate progress and cooperatively stopped occupying the executor.
05 · TUTORIAL
A Long Synchronous Job Does Not Cooperate Promptly
Consider this deliberately bad main-actor function:
CODE EXAMPLE
@MainActor
func calculateLaunchWindows() {
var result = 0
for value in 1...500_000_000 {
result &+= value
}
launchWindow = result
}
The loop contains one long synchronous sequence of instructions.
EXECUTION DIAGRAM
MAIN-ACTOR EXECUTOR
Calculation job
████████████████████████████████████████████
Countdown job
████
Touch-handling work
████
The countdown and touch-related work may already be ready.
However, the running job does not return the main execution opportunity until the calculation completes.
The executor can schedule eligible jobs. It cannot make the synchronous loop inexpensive or silently split it at arbitrary source-code lines.
💡 The Central Rule
A scheduler can choose among work that is eligible to run.
It cannot run a second serial-executor job until the current job returns control.
06 · TUTORIAL
async Does Not Make Every Function Cooperative
Adding async does not insert scheduling opportunities into synchronous code.
CODE EXAMPLE
@MainActor
func calculateLaunchWindows() async {
for value in 1...500_000_000 {
performStep(value)
}
}
The declaration permits asynchronous calls, but this implementation contains none.
The loop still runs as one synchronous job.
EXECUTION DIAGRAM
ASYNC DECLARATION
│
▼
No await reached
│
▼
No suspension occurs
│
▼
Current job continues until return
Cooperation depends on execution behaviour, not the visual presence of concurrency keywords around the function.
07 · TUTORIAL
Task {} Does Not Force Fairness
Creating several tasks does not guarantee that each task receives an equal turn immediately.
CODE EXAMPLE
Task { @MainActor in
calculateLaunchWindows()
}
Task { @MainActor in
status = "Second task ran"
}
If the calculation job runs first, the second task cannot update the same actor until that job finishes.
Task creation expresses multiple operation lifetimes. It does not promise round-robin scheduling between every line or loop iteration.
CODE EXAMPLE
NOT THIS
Task A: one loop iteration
Task B: one statement
Task A: one loop iteration
Task B: next statement
POSSIBLE REALITY ON ONE SERIAL EXECUTOR
Task A: complete synchronous job
Task B: complete next eligible job
The boundaries of runnable jobs matter.
08 · TUTORIAL
Suspension Creates a Scheduling Opportunity
An asynchronous wait naturally gives the executor another opportunity:
CODE EXAMPLE
Task { @MainActor in
print("A: begin")
try await Task.sleep(for: .seconds(2))
print("A: finish")
}
Task { @MainActor in
print("B: update interface")
}
A possible output is:
CODE EXAMPLE
A: begin
B: update interface
A: finish
EXECUTION DIAGRAM
MAIN ACTOR
Task A job 1
██████
│
▼ A suspends
Task B job
██████████████
Task A job 2
███████
Task A and Task B are concurrent because their lifetimes overlap.
Their main-actor jobs remain serial. Suspension allows their work to interleave without requiring simultaneous execution.
09 · TUTORIAL
What Does Task.yield() Do?
Task.yield() suspends the current task and offers the scheduler an opportunity to run other work.
CODE EXAMPLE
func processLaunches(_ launches: [Launch]) async {
for launch in launches {
process(launch)
await Task.yield()
}
}
This example offers a scheduling opportunity after every processed launch.
It does not guarantee that a different task will run next. If no higher-priority or equally eligible work is selected, the same task may continue.
💡 Reality Check
Task.yield() is a scheduling hint and suspension opportunity.
It is not a fairness guarantee, a delay, or a general solution for expensive CPU work.
10 · TUTORIAL
Should Long Calculations Repeatedly Yield?
Occasional yielding can help specialised algorithms cooperate when their work must remain in one asynchronous context.
But sprinkling Task.yield() through a large main-actor calculation is usually not the architectural goal.
CODE EXAMPLE
@MainActor
func calculateLaunchWindows() async {
for value in 1...500_000_000 {
performStep(value)
if value.isMultiple(of: 10_000) {
await Task.yield()
}
}
}
This may create opportunities for other main-actor jobs, but the expensive calculation still competes for the latency-sensitive main execution domain.
CPU-intensive model work normally needs an execution design appropriate for concurrent computation, with safe values crossing back to the UI's isolation domain.
The later articles on executors, isolation and Sendable will build that design properly.
11 · TUTORIAL
Cooperative Scheduling Is Not Processor Scheduling
Swift and the operating system schedule different things.
EXECUTION DIAGRAM
SWIFT CONCURRENCY
Executors arrange eligible task jobs
│
▼
SYSTEM THREADS EXECUTE THOSE JOBS
│
▼
OPERATING SYSTEM
Schedules runnable threads onto processor cores
The operating system may pre-empt a thread and run another thread.
That does not mean Swift has divided the current task job at a safe language-level boundary or allowed another job on the same serial executor to enter actor-isolated state.
OS thread pre-emption and Swift task cooperation are separate scheduling layers.
12 · TUTORIAL
Cooperation Does Not Mean One Global Queue
Swift does not place every task in the application onto one serial line.
Different executors can arrange different eligible jobs, and the runtime ultimately uses system execution resources underneath.
CODE EXAMPLE
MAIN-ACTOR EXECUTOR
Serial UI-isolated jobs
OTHER EXECUTION CONTEXTS
Other eligible task jobs
SYSTEM THREADS AND CPU CORES
Provide the underlying execution resources
Some work can progress concurrently, and suitable independent work may execute in parallel when resources and isolation allow it.
Cooperative scheduling does not mean the entire program runs one task at a time. It describes how task jobs expose boundaries at which runtime scheduling can make progress.
13 · TUTORIAL
Why Cooperation Matters to the Interface
A responsive application repeatedly needs short opportunities to process events and coordinate visible updates.
A main-actor task that suspends during a network wait gives those jobs a chance to run:
CODE EXAMPLE
@MainActor
func refreshLaunches() async throws {
status = "Loading"
launches = try await api.loadLaunches()
status = "Finished"
}
EXECUTION DIAGRAM
MAIN EXECUTION DOMAIN
Refresh job 1
████ status = "Loading" ████
│
▼ refresh task suspends
Event and UI-related jobs
█████████████████
Refresh job 2
████ update launches ████
Suspension does not command a render or guarantee which job executes next.
It returns the execution opportunity so other eligible main-actor and framework work can progress.
14 · TUTORIAL
A Cooperative Launch Feature
The API expresses waiting through suspension:
CODE EXAMPLE
struct LaunchAPI {
func loadLaunches() async throws -> [Launch] {
let (data, _) = try await URLSession.shared.data(
from: launchesURL
)
return try JSONDecoder().decode(
[Launch].self,
from: data
)
}
}
The feature keeps its synchronous main-actor periods short:
CODE EXAMPLE
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
@Published private(set) var isLoading = false
@Published private(set) var errorMessage: String?
private let api: LaunchAPI
init(api: LaunchAPI) {
self.api = api
}
func refresh() async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let result = try await api.loadLaunches()
launches = result
} catch {
errorMessage = error.localizedDescription
}
}
}
The first main-actor job establishes loading state and begins the asynchronous request.
The task suspends while no response is available. Other eligible jobs can use the released execution opportunity.
The continuation becomes eligible when the request finishes. A later main-actor job publishes the result.
The feature cooperates because its actor-isolated synchronous periods are short and its genuine wait suspends.
15 · TUTORIAL
The Complete Mental Model
EXECUTION DIAGRAM
TASK
│
├── runnable job
│ │
│ ▼
│ executor arranges execution
│ │
│ ▼
│ synchronous instructions run
│ │
│ ├── job completes
│ │
│ └── task suspends
│ │
│ ▼
│ execution opportunity returns
│ │
│ ▼
│ another eligible job can run
│
└── continuation becomes runnable later
Swift tasks are cooperative because their runnable jobs execute synchronously until they complete or reach suspension.
Executors choose among eligible jobs, but they cannot silently break a long synchronous job into fair turns.
Well-designed asynchronous code keeps synchronous jobs appropriately bounded and represents genuine waiting with suspension.
That is cooperative scheduling in Swift.
16 · TUTORIAL
What to Remember
💡 What to Remember
1. A task can contain several runnable jobs separated by suspension.
2. An executor arranges the execution of eligible jobs.
3. A running synchronous job normally continues until it completes or the task suspends.
4. The scheduler cannot make long synchronous work inexpensive.
5. Declaring a function async does not insert automatic scheduling opportunities.
6. Creating several tasks does not guarantee equal or round-robin turns.
7. A genuine suspension returns the execution opportunity for other eligible work.
8. Task.yield() offers a scheduling opportunity but does not guarantee another task runs next.
9. Swift task scheduling and operating-system thread scheduling are different layers.
10. Responsive code keeps latency-sensitive synchronous jobs short.
17 · TUTORIAL
Frequently Asked Questions
What is cooperative scheduling in Swift?
It is the model in which task jobs run synchronously until they complete or reach suspension, returning an execution opportunity that allows an executor to arrange another eligible job.
Can Swift interrupt a running Task?
The operating system can pre-empt the underlying thread, but Swift does not normally split arbitrary synchronous task code into separately scheduled jobs. At the Swift level, another job on the same serial executor needs the current job to return control.
Does await always let another Task run?
No. await marks a potential suspension point. If suspension is unnecessary, the task may continue. Even when suspension occurs, the scheduler decides which eligible work runs next.
Does Task.yield guarantee fairness?
No. It suspends the current task and offers a scheduling opportunity. It does not promise which task will be selected next or that another task must run first.
Why can async code still freeze the UI?
An asynchronous function can execute a long synchronous job on the main actor before reaching suspension or returning. During that job, other main-actor-dependent work remains delayed.
Is cooperative scheduling the same as serial execution?
No. A particular executor such as the main actor runs its isolated jobs serially, while other executors and system threads may execute other work concurrently or in parallel.
18 · TUTORIAL
Continue Learning Swift Concurrency
Read Suspension vs Blocking in Swift to revisit the execution boundary that enables cooperation.
Continue with What Is an Executor in Swift? to examine the service that accepts eligible task jobs and arranges where they execute.
Swift's official Concurrency documentation describes tasks, suspension and the language's concurrency model.
19 · TUTORIAL
Download Xcode Playground
The accompanying What Is Cooperative Scheduling in Swift? Xcode playground can make executor cooperation visible in the console.
It should run two main-actor tasks, first with one long synchronous job and then with a genuine suspension. The output will show why the second task cannot progress until the first returns control.
A separate experiment should use Task.yield() and explicitly demonstrate that the resulting order is an observation rather than a fairness guarantee.
The final page should run the complete launch feature while a countdown task continues updating, connecting short jobs and suspension to visible responsiveness.
The article explains the cooperation. The playground will reveal exactly where the execution opportunity changes hands.
