While we’ve covered both already in this module, I wanted to cover the differences between structured and unstructured tasks explicitly. In my experience, when discussing structured concurrency, there’s often a mention of both.
What Are Structured Tasks?
Structured tasks are part of a defined hierarchy, meaning they are bound to a parent task and inherit its lifecycle. This ensures better task management, predictable execution flow, and a shared cancellation state. The latter means structured tasks are notified about a cancellation, which they can respect through code like try Task.checkCancellation() (more on this later in this course).
There are a few key characteristics of structured tasks:
- Scoped to a parent: They are tied to an existing task, actor, or task group.
- Automatic cancellation: If the parent task is canceled, all child tasks are also notified about the cancellation.
- Predictable execution flow: The compiler enforces structured concurrency rules, reducing the risk of leaks or orphaned tasks.
Examples of Structured Tasks
I’ll give a few examples to get a better sense of structured tasks.
- async let: Used for launching concurrent tasks while keeping the structure clear, as discussed in the dedicated lesson in this module.
async let result1 = fetchDataOne()
async let result2 = fetchDataTwo()
async let result3 = fetchDataThree()
let results = await [result1, result2, result3]
- Task Groups: Dynamically create tasks within a group while maintaining structured execution.
await withTaskGroup(of: String.self) { group in
group.addTask { await fetchDataOne() }
group.addTask { await fetchDataTwo() }
for await result in group {
print(result)
}
}
What Are Unstructured Tasks?
Unstructured tasks run independently of any parent task, making them more flexible and prone to mismanagement. These tasks need to be manually handled for cancellation and lifecycle management.
They have the following key characteristics:
- Not tied to a parent: They exist independently and don’t automatically inherit cancellation behavior.
- Manual cancellation required: Developers need to manage task cancellation explicitly.
- More flexibility, but more risk: While they offer more control, they also introduce potential pitfalls like race conditions or orphaned tasks.
Examples of Unstructured Tasks
Here are a few examples to better understand unstructured tasks.
- Detached Tasks: It’s in the word: ‘detached’. Created using
Task.detached, these tasks operate independently, making them useful for long-running background work.
let task = Task.detached {
return await fetchData()
}
- Explicitly Created Tasks: These are not detached but still unstructured, requiring manual cancellation handling.
let task = Task {
await fetchData()
}
task.cancel()
Looking at the last example, you could argue all tasks have to start somewhere in an ‘unstructured Task’ container. Yet, within that container, all tasks run in a structured environment.
Choosing Between Structured and Unstructured Tasks
As mentioned before in the lesson on detached tasks, you should try to avoid unstructured tasks as much as possible. The Task { } wrapper is obviously one you will use, but detached tasks should only be needed when you’re running independent, long-running tasks that don’t require automatic cancellation, predictable execution, or a parent-child relationship.
Summary
Structured tasks should be preferred for better task management and safety. Unstructured tasks should be used sparingly and only when necessary for tasks that require independence. Understanding these distinctions helps you write safer, more predictable concurrent Swift code.
In the next lesson, we will look at task priorities to influence the execution order.