What does Cooperative Concurrency Model Mean?
Understand Swift's cooperative concurrency model and how it differs from pre-emptive concurrency.
Concurrency models dictate how tasks multitask and handle cancellation. Swift’s concurrency follows the cooperative concurrency model, contrasting with the pre-emptive model. This article clarifies both models, highlights why Swift chose cooperative concurrency, and demonstrates practical implications through clear examples.
1. Pre‑emptive vs. Cooperative: The Big Picture
| Aspect | Pre‑emptive | Cooperative |
|---|---|---|
| Pausing Tasks | Scheduler interrupts tasks at regular intervals. | Tasks voluntarily yield at defined suspension points. |
| Context Switches | Can occur at unpredictable points, even mid-operation. | Occur only at explicit points (await, Task.yield()). |
| Cancellation | Immediate, can happen mid-execution. | Checked only at suspension points. |
| Overhead | High due to frequent interruptions. | Lower, due to targeted yielding. |
| Fairness & Responsiveness | Good fairness, potential overhead. | Efficient, but tasks must yield proactively to remain responsive. |
2. Why Swift Uses Cooperative Concurrency
Swift’s cooperative concurrency approach offers key advantages:
- Efficiency: Low overhead as the scheduler only switches tasks at explicit suspension points.
- Predictability: Developers explicitly define suspension points, simplifying code reasoning.
- Safety: Complements Swift’s data-race protections (actor isolation, Sendable enforcement), maintaining clear boundaries and invariants.
3. Suspension Points as Yield Points
Every await is a clear suspension point in Swift, performing these operations:
- Saves current task’s context (stack, registers).
- Scheduler runs another ready task.
- Resumes original task upon awaited task completion.
Cancellation checks occur exclusively at these points, meaning tasks won’t interrupt spontaneously.
4. Practical Examples
A. Uncancellable Busy-Loop
1
2
3
4
5
6
7
8
func crunchNumbers(_ max: Int) async {
var total = 0
for i in 1...max {
total += i * i
// No suspension points; can't cancel mid-execution.
}
print("Done: \(total)")
}
Result: Task ignores cancellation requests until completion.
B. Cancellable Loop with Task.yield()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func crunchNumbers(_ max: Int) async {
var total = 0
for i in 1...max {
total += i * i
if i % 1_000 == 0 {
await Task.yield() // Explicit suspension & cancellation check
if Task.isCancelled {
print("Crunch cancelled at i = \(i)")
return
}
}
}
print("Done: \(total)")
}
Result: Task becomes cancellable every 1000 iterations, allowing responsive cancellations.
C. Network Calls & Natural Suspension
1
2
3
4
5
6
7
8
9
10
11
12
func downloadImages(urls: [URL]) async throws {
for url in urls {
let (data, _) = try await URLSession.shared.data(from: url) // Natural suspension
if Task.isCancelled {
print("Download cancelled before processing \(url)")
return
}
process(data)
}
}
Result: Cancellation checks happen naturally at each network call.
5. Cooperative Cancellation with Task.checkCancellation()
Swift provides Task.checkCancellation(), combining suspension and immediate cancellation checks:
1
2
3
4
5
6
func runSteps() async throws {
for step in 1...10 {
try Task.checkCancellation() // Suspends and checks cancellation
// Perform heavy step work...
}
}
Equivalent to:
1
2
await Task.yield()
if Task.isCancelled { throw CancellationError() }
6. Summary
- Pre-emptive: Tasks interrupted unpredictably; higher overhead.
- Cooperative: Explicit, predictable suspension points; efficient.
- Why Cooperative in Swift? Small tasks, predictability, data-safety integrations.
- Developer Responsibility: Use
await Task.yield()andTask.checkCancellation()to maintain task responsiveness.
☕ Support My Work
If you found this post helpful and want to support more content like this, you can buy me a coffee!
Your support helps me continue creating useful articles and tips for fellow developers. Thank you! 🙏