Member-only story
Why is my Task running on the main thread?
Building an intuition for Swift Concurrency
Tasks are the unit of async work in Swift Concurrency. Creating a Task is the simplest way to introduce an async context to your code:
func setupView() {
Task {
self.data = await fetchData()
}
}
Using a Task, we can call await
and fetch data asynchronously even though setupView()
itself is not marked async
.
Because we’re initialising them with a closure, the standard* behaviour of creating a Task isn’t immediately obvious: they’ll run on the same thread they’re created on.
Let’s explore the implications this has on our code.
*I’ll come back to this, I promise.
Blocking Synchronous Workloads
Recently, I helped debug a weird hang — a UI freeze — that showed up from a viewDidLoad
method (remember those?).
The culprit was a synchronous database call which performed a complex read query and processed the results. This blocked our UI, since the processing had to complete before the screen was presented.
@MainActor
override func viewDidLoad() {
super.viewDidLoad()
self.data = fetchDataFromDatabase()
}
Let’s try the first obvious solution: get this code off the main thread. The naïve approach is to create a Task and call it a day.
@MainActor
override func viewDidLoad() {
super.viewDidLoad()
Task {
self.data = fetchDataFromDatabase()
}
}
Remember that a Task will normally run on the thread it was instantiated from: unless you explicitly allow a context switch with await
, execution will be bound to the thread of the actor it was called from.
Therefore, since this function is on the @MainActor
, the contents of the Task will still run on the main thread — the original UI hang isn’t fixed.
We can create our own sample code to see this principle in action.
