TaskGroup: Mastering Dynamic Parallelism in Swift
Published on · Updated · 20 min
TaskGroup is the API of choice for executing an arbitrary number of tasks in parallel and collecting their results. Unlike async let which requires knowing the number of tasks at compile time, TaskGroup allows creating tasks dynamically at runtime. This guide covers everything from fundamentals to advanced patterns with DiscardingTaskGroup.
Why TaskGroup?
async let is perfect when you know the exact number of tasks:
But what if you need to download 1000 images? Writing 1000 async let statements isn't feasible. That's where TaskGroup comes in:
TaskGroup Variants
Swift provides four types of task groups:
| Type | Throwing | Results |
|---|---|---|
| No | Collected |
| Yes | Collected |
| No | Discarded |
| Yes | Discarded |
Basic Syntax
Creating and Adding Tasks
Basic withTaskGroup
Task Priority
Each task can have its own priority:
addTaskUnlessCancelled
To avoid adding tasks if the group is already cancelled:
Collecting Results
Iteration with for await
The most common method for collecting results:
Important: The order of results is NOT guaranteed. Tasks complete in any order depending on their duration.
Preserving Order with Tuples
To maintain the original order, include the index:
Using reduce
To aggregate results directly:
Using next()
For fine-grained control over result consumption:
waitForAll()
To wait for all tasks without collecting results:
Error Handling with ThrowingTaskGroup
Default Behavior: Fail-Fast
With withThrowingTaskGroup, the first error is propagated and the group is cancelled:
Collect-All: Continue Despite Errors
To collect all results even if some tasks fail:
Dashboard Pattern: Independent Sections
Cancellation
cancelAll()
To cancel all remaining tasks:
isCancelled and checkCancellation
Tasks must check for cancellation cooperatively:
Automatic Cancellation on Exit
The group always waits for all tasks before returning, even after cancelAll():
DiscardingTaskGroup (Swift 5.9+)
The Problem with Classic TaskGroup
With a standard TaskGroup, if you don't call next() or iterate over the group, completed tasks remain in memory:
Solution: DiscardingTaskGroup
DiscardingTaskGroup automatically cleans up completed tasks:
Ideal Use Cases
DiscardingTaskGroup is perfect for:
- HTTP/WebSocket servers: infinite connection loops
- File watchers: continuous file monitoring
- Event listeners: continuous event processing
- Background workers: tasks with no results to collect
Differences from TaskGroup
| Aspect | TaskGroup | DiscardingTaskGroup |
|---|---|---|
Results | Collected via next()/for await | Immediately discarded |
Memory | Tasks kept until consumed | Released when completed |
AsyncSequence | Yes | No |
next() | Available | Not available |
Use case | Collect results | Fire-and-forget, servers |
Advanced Patterns
Limiting Concurrency
TaskGroup doesn't limit the number of concurrent tasks by default. Here's how to do it:
Global Timeout
Race: First Come, First Served
Retry with Parallelism
Heterogeneous Types with Enum
For tasks that return different types:
Best Practices
1. Always Consume Results or Use DiscardingTaskGroup
2. Handle Cancellation in Tasks
3. Avoid Strong Captures of self
4. Use the Right Group Type
| Situation | Recommended Type |
|---|---|
Collect results |
|
Tasks that can fail |
|
Fire-and-forget |
|
Server/infinite loop |
|
Common Pitfalls
1. Forgetting That Order Is Not Guaranteed
2. Not Handling Errors Correctly
3. Creating Too Many Tasks at Once
Going Further
TaskGroup is the essential tool for dynamic parallelism in Swift. Combined with async let for simple cases and DiscardingTaskGroup for servers, you have everything you need to write performant and safe concurrent code.