Issue #904
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 thatTask
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
Questions on Swift Forum
Which Task block the main thread?
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.
Determining whether an async function will run on the main actor
The essential rule to know is:
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.
Flavors of Task
Explore structured concurrency in Swift
An asynchronous call
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
Visualize and optimize Swift concurrency
Work can be moved into the background by putting it in a normal Actor or in a detached task.
Eliminate data races using Swift Concurrency
Non-isolated async code always runs on the global cooperative pool.
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.
Read more
- https://www.hackingwithswift.com/quick-start/concurrency/whats-the-difference-between-a-task-and-a-detached-task
- https://stackoverflow.com/questions/71837201/task-blocks-main-thread-when-calling-async-function-inside
- https://stackoverflow.com/questions/73958785/does-await-use-main-thread-or-cooperative-thread-pool
- https://oleb.net/2022/swiftui-task-mainactor/