Consider this code where we have an ObservableObject with fetch1 and async fetch2, and a fetch inside ContentView
Here the observation in Xcode 14
ViewModel.fetch1: run on main thread
ViewModel.fetch2: run on cooperative thread pool
ContentView.fetch: run on main thread
import SwiftUI
import CoreData
import Combine
class ViewModel: ObservableObject {
@Published var string = ""
func fetch1() {
let url = URL(string: "https://google.com")!
let data = try! Data(contentsOf: url)
self.string = String(data: data, encoding: .utf8) ?? ""
}
func fetch2() async {
let url = URL(string: "https://google.com")!
let data = try! Data(contentsOf: url)
self.string = String(data: data, encoding: .utf8) ?? ""
}
}
struct ContentView: View {
@State var string = ""
@StateObject var vm = ViewModel()
var body: some View {
VStack {
Button {
Task {
await vm.fetch1()
}
Task {
await vm.fetch2()
}
Task {
await fetch()
}
} label: {
Text("Fetch")
}
Text(string)
Text(vm.string)
}
}
private func fetch() async {
let url = URL(string: "https://google.com")!
let data = try! Data(contentsOf: url)
self.string = String(data: data, encoding: .utf8) ?? ""
}
}
In the example above, the work Data(contentsOf is synchronously blocking so it blocks whatever thread it is executed on.
ContentView.fetch is running on the main actor, since we have StateObject declaration in a SwiftUI view, which turns the whole view to be run on the main actor.
ViewModel.fetch1 is not marked as async, so it blocks the thread that Task is spawned. Task inherits the async context where is spawned from, in this case, the main actor
ViewModel.fetch2 is an async function. Although the Task is spawned in the context of the main actor, it has suspension point in the await, and Swift concurrency uses the cooperative thread pool to execute the work
Swift 5.7 async functions that are not actor-isolated should formally run on a generic executor associated with no actor. Such functions will formally switch executors exactly like an actor-isolated function would: on any entry to the function, including calls, returns from calls, and resumption from suspension, they will switch to a generic, non-actor executor. If they were previously running on some actor's executor, that executor will become free to execute other tasks.
Synchronous functions always run on the thread they were called from
Asynchronous functions always choose where they run, no matter where they were called from.
If it is isolated to an actor, it runs on that actor
Otherwise, it runs on the default executor, which is not the main thread.
With these annotations made explicit, it's easier to explain what's happening.
All three Task closures are @MainActor isolated, so any synchronous code within them runs on the main thread.
fetch1 is a synchronous function, so the await does nothing when you call it. The task it's called from is @MainActor-isolated, so fetch1() also runs on the main thread in this example. (If you were to call it from a background thread, it would run in the background.)
fetch2 is an async function, so it decides where it runs. Since it's not isolated to an actor, it runs on the default executor, which on Apple platforms is a pool of threads that runs in the background, and thus not on the main thread. Even though it was called from the main thread, it still runs in the background.
fetch3 is an async function, so it decides where it runs. Since it is @MainActor isolated, it always runs on the main thread, no matter where it was called from.
Once it’s running, an async function can suspend. When it does, it gives up control of the thread. But rather than giving control back to your function, it instead gives control of the thread to the system. When that happens, your function is suspended too
Now, this is also true when you use completion handlers. But because you don’t have all the ceremony and indentation they entail in async/await code, the await keyword is how you notice that a block of code doesn’t execute as one transaction. The function may suspend, and other things may happen while it’s suspended between the lines of the function.
More than that, the function may resume onto an entirely different thread
When calling an asynchronous method, execution suspends until that method returns. You write await in front of the call to mark the possible suspension point. This is like writing try when calling a throwing function, to mark the possible change to the program’s flow if there’s an error.
Consider this code where we have an
ObservableObject
withfetch1
and asyncfetch2
, and afetch
insideContentView
Here the observation in Xcode 14
In the example above, the work
Data(contentsOf
is synchronously blocking so it blocks whatever thread it is executed on.StateObject
declaration in a SwiftUI view, which turns the whole view to be run on the main actor.async
, so it blocks the thread thatTask
is spawned. Task inherits the async context where is spawned from, in this case, the main actorawait
, and Swift concurrency uses the cooperative thread pool to execute the workQuestions on Swift Forum
Which Task block the main thread?
Determining whether an async function will run on the main actor
Flavors of Task
Explore structured concurrency in Swift
An asynchronous call
Meet async/await in Swift
Visualize and optimize Swift concurrency
Eliminate data races using Swift Concurrency
Concurrency
Read more