Why does a Task within a #MainActor not block the UI? - swift

Today I refactored a ViewModel for a SwiftUI view to structured concurrency. It fires a network request and when the request comes back, updates a #Published property to update the UI. Since I use a Task to perform the network request, I have to get back to the MainActor to update my property, and I was exploring different ways to do that. One straightforward way was to use MainActor.run inside my Task, which works just fine. I then tried to use #MainActor, and don't quite understand the behaviour here.
A bit simplified, my ViewModel would look somewhat like this:
class ContentViewModel: ObservableObject {
#Published var showLoadingIndicator = false
#MainActor func reload() {
showLoadingIndicator = true
Task {
try await doNetworkRequest()
showLoadingIndicator = false
}
}
#MainActor func someOtherMethod() {
// does UI work
}
}
I would have expected this to not work properly.
First, I expected SwiftUI to complain that showLoadingIndicator = false happens off the main thread. It didn't. So I put in a breakpoint, and it seems even the Task within a #MainActor is run on the main thread. Why that is is maybe a question for another day, I think I haven't quite figured out Task yet. For now, let's accept this.
So then I would have expected the UI to be blocked during my networkRequest - after all, it is run on the main thread. But this is not the case either. The network request runs, and the UI stays responsive during that. Even a call to another method on the main actor (e.g. someOtherMethod) works completely fine.
Even running something like Task.sleep() within doNetworkRequest will STILL work completely fine. This is great, but I would like to understand why.
My questions:
a) Am I right in assuming a Task within a MainActor does not block the UI? Why?
b) Is this a sensible approach, or can I run into trouble by using #MainActor for dispatching asynchronous work like this?

await is a yield point in Swift. It's where the current Task releases the queue and allows something else to run. So at this line:
try await doNetworkRequest()
your Task will let go of the main queue, and let something else be scheduled. It won't block the queue waiting for it to finish.
This means that after the await returns, it's possible that other code has been run by the main actor, so you can't trust the values of properties or other preconditions you've cached before the await.
Currently there's no simple, built-in way to say "block this actor until this finishes." Actors are reentrant.

Related

Do async calls within a function marked with #MainActor also run on the main thread?

I have a function marked with #MainActor that makes an async call, then updates a #Published var that will subsequently update the UI. Is the async call blocking on the main thread, or is the updating of the published var the only thing that will be done on the main thread?
#Published var users: [User] = []
#MainActor
func getUsers() {
Task {
// Is the async call blocking on the main thread here?
users = await userRepository.getUsers()
}
}
In the above example, is users being updated on the main thread, while the async call happening on the UserRepository being done in the background? Or, is that async call also being run and blocking on the main thread?
The Task initialiser
Task(priority:operation:)
inherits the priority and actor context of the caller, so in your case, as the func is marked #MainActor, it will run on the main queue.
userRepository.getUsers() runs on another queue, then when it returns your func continues back on the main queue.
While it's running the main queue isn't blocked, the func itself is suspended.
First of all Swift Concurrency Tasks don't block the current thread literally.
A Task inherits the execution context from its parent (unless it's a detached Task).
Even if userRepository.getUsers() runs on a background thread users is being updated on the main thread
I know your question has been answered, but I wanted to offer a few clarifying observations.
You asked:
I have a function marked with #MainActor that makes an async call, then updates a #Published var that will subsequently update the UI. Is the async call blocking on the main thread …
Nothing here will block the main thread. And because your method has been marked with #MainActor, the properties it updates will also happen to occur on the main actor, too. (This is a bit brittle, though, because the burden now rests on your shoulders, having to make sure that any methods that happen to update these properties do so on the main actor. But more on that later.)
Technically, we really need to see the userRepository implementation to make sure it doesn’t block the current actor. But assuming the “user repository” is not doing something silly like doing some slow synchronous, computationally intensive calculation that blocks the main thread, you should be fine.
The key observation, though, is that unlike older “wait” API (such as GCD’s DispatchGroup), the Swift concurrency await keyword does not, as a rule, block the current thread. The await keyword says, “temporarily suspend this function, and free the current thread/actor to go do other stuff until the called async function returns a value.”
… or is the updating of the published var the only thing that will be done on the main thread?
It turns out that this whole method will actually run on the main actor. But there is not much here, though, so that is not a problem. It is just calling an async method and then updating some #Published property. (In terms of what actor the “user repository” is using, that’s not clear. That is dictated by how that repository was implemented. But as long as the “user repository” doesn't violate that basic Swift concurrency “runtime contract” to not impede forward progress, it does not really matter.)
You go on to ask:
In the above example, is users being updated on the main thread, while the async call happening on the UserRepository being done in the background? Or, is that async call also being run and blocking on the main thread?
There is no “blocking” of the thread. We are “awaiting” a result, and the current thread/actor is free to do other things in the interim.
All of this having been said, I might suggest a different pattern. While we have hopefully shown that await avoids blocking at all, if your key concern is that you want these #Published properties to be published on the main actor, then you want mark those properties as being #MainActor. If you do that, then the compiler will ensure that they are updated from the main actor (and generate compilation errors if you ever accidentally attempt to update them from something other than the main actor). It is a more robust way to make sure your published properties happen on the main actor.
Taking this a step further, if you have some class whose primary purpose is to update #Published properties for a view, you might mark that whole class as #MainActor. Then you do not have to mark all of the individual #Published properties with #MainActor.
E.g., consider the following:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(viewModel.users) { user in
Text(user.name)
}
}
.padding()
.task {
await viewModel.getUsers()
}
}
}
#MainActor
class ViewModel: ObservableObject {
let userRepository = UserRepository()
#Published var users: [User] = [] // automatically `#MainActor` because the whole class is
func getUsers() async {
users = await userRepository.getUsers()
}
}
actor UserRepository {
func getUsers() async -> [User] {
…
}
}
Now, in the above example, I have made the UserRepository an actor. That is not terribly relevant, but it illustrates that repositories, services, managers, etc., do not have to be on the main actor. The key observation is that you may consider just using #MainActor for the whole class whose primary role is to publishing property updates for the UI.
Also, as an aside, where possible, I would advise staying within structured concurrency. Note how I started the request in the .task {…} modifier. That means that when the view is presented, the request will be initiated, but if the view is dismissed, the request will be automatically canceled for me. When you create a Task (e.g., with Task {…}), that is unstructured concurrency, with no automatic cancelation. Now, if you are in a non-asynchronous context (e.g., in UIKit or AppKit), then you have to do what you have to do. But with SwiftUI, consider using the .task {…} modifier.
If you are interested in diving into the Swift concurrency threading model (and the aforementioned “runtime contract”), I might suggest watching WWDC 2021 video Swift concurrency: Behind the scenes. While we generally want to stop worrying about thread-level details when writing Swift concurrency code, this video provides some insights about some of the clever threading logic going on behind the scenes.
I've noticed Apple teaching it this way around instead:
// not main actor
class MyClass: ObservableObject {
#Published var users: [User] = []
// not main actor
func getUsers() async {
// background thread
let users = await userRepository.getUsers() // should also be background thread if neither the class or func are main actor.
Task { #MainActor in
// main thread
self.users = users
}
}
...
// In your View:
.task {
// main thread
await myClass.getUsers() // background thread because myClass and getUsers are not main actor.
}
https://developer.apple.com/tutorials/sample-apps/memecreator#Fetching-Panda-Data

Best practice with asynchronous functions Swift & Combine

I'm converting my Swift app to use Combine as well as async/await and I'm trying to understand what's the best way to handle interactions between asynchronous functions and the main thread.
Here's an asynchronous function that loads a user:
class AccountManager {
static func fetchOrLoadUser() async throws -> AppUser {
if let user = AppUser.current.value {
return user
}
let syncUser = try await loadUser()
let user = try AppUser(syncUser: syncUser)
AppUser.current.value = user // [warning]: "Publishing changes from background threads is not allowed"
return user
}
}
And a class:
class AppUser {
static var current = CurrentValueSubject<AppUser?,Never>(nil)
// ...
}
Note: I chose to use CurrentValueSubject because it allows me to both (1) read this value synchronously whenever I need it and (2) subscribe for changes.
Now, on the line marked above I get the error Publishing changes from background threads is not allowed, which I understand. I see different ways to solve this issue:
1. Mark whole AccountManager class as #MainActor
Since most of the work done in asynchronous functions is to wait for network results, I'm wondering if there is an issue with simply running everything on the main thread. Would that cause performance issues or not?
2. Englobe error line in DispatchQueue.main.sync
Is that a reasonable solution, or would that cause threading problems like deadlocks?
3. Use DispatchGroup with enter(), leave() and wait()
Like in this answer. Is there a difference at all with solution #2? Because this solution needs more lines of code so I'd rather not use it if possible —I prefer clean code.
You can wrap the call in an await MainActor.run { } block. I think this is the most Swifty way of doing that.
You should not use Dispatch mechanism while using Swift Concurrency, event though I think DispatchQueue.main.async { } is safe to use here.
The #MainActor attribute is safe but shouldn’t be used on anObservableObject and could potentially slow down the UI if CPU-bound code is run in a method of the annotated type.

How to use URLSession in MainActor

I have a ViewModel that is isolated with #MainActor and I assume that every part of this ViewModel class should run on the main actor (main thread), right?
So the question is, what about the async call of URLSession.shared.data?
Does it also run on main thread? Isn't that a bad approach?
#MainActor
class ViewModel {
#Published var name: String = ""
func fetchName() async {
guard let (data, _) = try? await URLSession.shared.data(from: URL(string: "http://....")!),
let response = try? JSONDecoder().decode(Response.self, from: data) else {
return
}
self.name = response.name
}
}
Does it also run on main thread
No. That's the whole point of saying await. At that moment, the system can switch contexts to a background thread without you knowing or caring. Moreover, at await your code pauses without blocking — meaning that while you're waiting for the download, the main thread is free to do other work, the user can interact with your app, etc. Don't worry be happy.
The issue is not the URLSession code that you await. That runs on a separate thread managed by URLSession. Whenever you see await, that means that the current execution is suspended while the asynchronous task runs, and that the current actor is free to run other code. All is well.
The only potential concern is code that runs synchronously on the main actor (i.e., everything besides what we await). In this case, the only relevant portion is the code after the await, inside what is called the “continuation”.
In this case, the continuation consists of the decode of the JSON and the updating of the property. That is modest and you are generally fine running that on the main actor.
But if the continuation consisted of anything anything more substantial (e.g. decoding many megabytes of JSON or decoding an image or the like), then you would want to move that off the main actor. But in this case, you are fine.
For more information, see WWDC 2021 video Swift concurrency: Behind the scenes.

How can I avoid that my Swift async method runs on the main thread in SwiftUI?

I watched all the videos on async/await (and actors), but I am still confused a bit.
So assume I have an async method: func postMessage(_ message: String) async throws and I have a simple SwiftUI view.
#MainActor
struct ContentView: View {
#StateObject private var api = API()
var body: some View {
Button("Post Message") {
Task {
do {
try await api.postMessage("First post!")
} catch {
print(error)
}
}
}
}
}
Here I explicitly tell SwiftUI to use the #MainActor although I know it would have inferred from #StateObject.
To my understanding, since we use the #MainActor the work is done on the main thread. Meaning the work on Task would also be done on the main thread. Which is not what I want as the uploading process might take some while. In this case I could use Task.detached to use a different thread. Which would solve it. If my understanding is correct.
Now to make it a little more complicated. What if... postMessage would return a post identifier as an integer and I like to present that in the view?
struct ContentView: View {
#StateObject private var api = API()
#State private var identifier: Int = 0
var body: some View {
Text("Post Identifier: \(String(describing: identifier))")
Button("Post Message") {
Task {
do {
identifier = try await api.postMessage("First post!")
} catch {
identifier = 0
print(error)
}
}
}
}
}
This would work as (again to my understanding) Task is run on the main thread. If I would change it now to Task.detached we will get an error "Property 'identifier' isolated to global actor 'MainActor' can not be mutated from a non-isolated context".
Which makes sense, but how can we return the value to the main actor so the view can be updated?
Perhaps my assumptions are wrong. Let's look at my API class.
actor API {
func postMessage(_ message: String) async throws -> Int {
// Some complex internet code
return 0
}
}
Since the API runs in its own actor. Would the internet work also run on a different thread?
The question is very good, and answer is complex. I have spent significant time on this topic, for detailed information please follow the link to Apple developer forum in comments.
All tasks mentioned below are unstructured tasks, eg. created by Task ...
This is key: "The main actor is an actor that represents the main thread. The main actor performs all of its synchronization through the main dispatch queue.
Unstructured tasks created by Task.init eg. Task { ... } inherit the actor async context.
Detached tasks Task.detached, async let = tasks, group tasks do NOT inherit actor async context.
Example 1: let task1 = Task { () -> Void in ... } creates and starts new task that inherits priority and async context from the point, where it is called. When created on the main thread, the task will run on the main thread.
Example 2: let task1 = Task.detached { () -> Void in ... } creates and starts new task that does NOT inherit priority nor async context. The task will run on some thread, very likely on other thread than is the current thread. The executor decides that.
Example 3: let task1 = Task.detached { #MainActor () -> Void in ... } creates and starts new task that does NOT inherit priority nor async context, but the task will run on the main thread, because it is annotated so.
Very likely, the task will contain at least one await or async let = command. These commands are part of structured concurrency, and you cannot influence, on what thread are the implicitly created tasks (not discussed here at all) executed. The Swift executor decides that.
The inherited actor async context has nothing to do with threads, after each await the thread may change, however the actor async context is kept throughout all the unstructured task (yes, may be on various threads, but this is important just for the executor).
If the inherited actor async context is MainActor, then the task runs on the main thread, from beginning till its end, because the actor context is MainActor. This is important if you plan to run some really parallel computation - make sure all the unstructured tasks do not run on the same thread.
ContentView is on #MainActor in both the cases: in the first case is it #MainActor explicitely, the second case uses #StateObject property wrapper that is #MainActor, so the whole ContentView structure is #MainActor inferred. https://www.hackingwithswift.com/quick-start/concurrency/understanding-how-global-actor-inference-works
async let = is a structured concurrency, it does not inherit actor async context, and runs in parallel immediately, as scheduled by the executor (on some other thread, if available)
The example on top has one system flaw: #StateObject private var api = API() is #MainActor. This is forced by the #StateObject. So, I would recommend to inject other actor with other actor async context as a dependency without #StateObject. The async/await will really work, keeping await calls with the proper actor contexts.
You said:
To my understanding, since we use the #MainActor the work is done on the main thread. Meaning the work on Task would also be done on the main thread. Which is not what I want as the uploading process might take some while.
Just because the uploading process (a) might take some time; and (b) was invoked from the main actor, that does not mean that the main thread will be blocked while the request is performed.
In fact, this is the whole point of Swift concurrency’s await. It frees the current thread to go do other things while we await results from the called routine. Do not conflate the await of Swift concurrency (which will not block the calling thread) with the various wait constructs of traditional GCD patterns (which will).
E.g., consider the following code launched from the main actor:
Task {
do {
identifier = try await api.postMessage("First post!")
} catch {
identifier = 0
print(error)
}
}
Yes, because you used Task and invoked it from the main actor, that code will also run on the main actor. Thus, identifier will be updated on the main actor. But it says nothing about which actor/queue/thread that postMessage is using. The await keyword means “I'll suspend this path of execution, and let the main thread and go do other things while we await the postMessage initiating its network request and its eventual response.”
You asked:
Let's look at my API class.
actor API {
func postMessage(_ message: String) async throws -> Int {
// Some complex internet code
return 0
}
}
Since the API runs in its own actor. Would the internet work also run
on a different thread?
As a general rule, networking libraries perform their requests asynchronously, not blocking the threads from which they are called. Assuming the “complex internet code” follows this standard convention, you then you simply do not have to worry about what thread it is running on. The actor’s thread will not be blocked while the request runs.
Taking this a step further, the fact that the API is its own actor is immaterial to the question. Even if API was on the main actor, because the network request runs asynchronously, whatever the thread used by API will not be blocked.
(NB: The above assumes that the “complex internet code” has followed conventional asynchronous network programming patterns. We obviously cannot comment further without seeing a representative example of this code.)
See WWDC 2021 video Swift concurrency: Behind the scenes if you are interested in how Swift concurrency manages threads for us.
The Task is the bridge between the Async and the Synchronous world. The completion block of Button being the synchronous world and the postMessage method as being the asynchronous world.
The Task is run on the #MainActor however since we await the result of api.postMessage it may suspend and the main thread is not blocked.
Thanks to Quang Hà and Lorem Ipsum.
Useful information regarding this question from WWDC.
From Discover concurrency in SwiftUI (Timecode - 7:24)
To update correctly, SwiftUI needs these events to happen in order: objectWillChange, the ObservableObject’s state is updated, and then the run loop reaches its next tick. If I can ensure that these all happen on the main actor, I can guarantee this ordering. Prior to Swift 5.5, I might have dispatched back to the main queue to update my state, but now it’s much easier. Just use await! By using await to make an async call from the main actor, I let other work continue on the main actor while the async work happens. This is called “yielding” the main actor.
From Meet async/await in Swift (Timecode - 22:13):
The solution is to use the async task function. An async task packages up the work in the closure and sends it to the system for immediate execution on the next available thread, like the async function on a global dispatch queue.
From Protect mutable state with Swift actors (Timecode - 7:25)
If the actor is busy, then your code will suspend so that the CPU you're running on can do other useful work. When the actor becomes free again, it will wake up your code -- resuming execution -- so the call can run on the actor. The await keyword in this example indicates that the asynchronous call to the actor might involve such a suspension.
From Protect mutable state with Swift actors (Timecode - 23:47)
When you are building an app, you need to think about the main thread. It is where the core user interface rendering happens, as well as where user interaction events are processed. Operations that work with the UI generally need to be performed from the main thread. However, you don't want to do all of your work on the main thread. If you do too much work on the main thread, say, because you have some slow input/output operation or blocking interaction with a server, your UI will freeze. So, you need to be careful to do work on the main thread when it interacts with the UI but get off the main thread quickly for computationally expensive or long-waiting operations. So, we do work off the main thread when we can and then call DispatchQueue.main.async in your code whenever you have a particular operation that must be executed on the main thread.
If you know you're already running on the main thread, you can safely access and update your UI state. If you aren't running on the main thread, you need to interact with it asynchronously. This is exactly how actors work.
The main actor is an actor that represents the main thread. It differs from a normal actor. The main actor performs all of its synchronization through the main dispatch queue. This means that, from a runtime perspective, the main actor is interchangeable with using DispatchQueue.main.

GCD serial queue like approach using swift async/await api?

I am adopting the new async/await Swift API. Things work great.
Currently in my apps I am using GCD serial queues as a pipeline to enforce tasks to happen serially.
For manipulating data of some critical models in the app I use a serial queue accessible from anywhere in the app:
let modelQueue = DispatchQueue(label: "com.myapp.updatemodelqueue")
Anytime a model needs to modify some model data I dispatch to that queue:
modelQueue.async {
// ... model updates
}
With the new async/await changes I am making I still want to force model actual updates to happen serially. So, for example, when I import new model data from the server I want to make sure they happen serially.
For example, I may have a call in my model that looks like this:
func updateCarModel() async {
let data = await getModelFromServer()
modelQueue.async {
// update model
}
}
Writing that function using that pattern however would not wait for the model update changes because of the modelQueue.async. I do not want to use modelQueue.sync to avoid deadlocks.
So then after watching WWDC videos and looking at documentation I implemented this way, leveraging withCheckedContinuation:
func updateCarModel() async {
let data = await getModelFromServer()
await withCheckedContinuation({ continuation in
modelQueue.async {
// update model
continuation.resume()
}
})
}
However, to my understanding withCheckedContinuation is really meant to allow us to incrementally transition to fully adopt the new async/await Swift API. So, it does not seem to be what I should use as a final approach.
I then looked into actor, but I am not sure how that would allow me to serialize any model work I want to serialize around the app like I did with a static queue like shown above.
So, how can I enforce my model around the app to do model updates serially like I used to while also fully adopting the new await/async swift API without using withCheckedContinuation?
By making the model an actor, Swift synchronizes access to it' shared mutable state. If the model is written like this:
actor Model {
var data: Data
func updateModel(newData: Data) {
data = newData
}
}
The updateModel function here is synchronous, it's execution is uninterrupted after it's invoked. Because Model is an actor, Swift restricts you to treat it as if you are calling an asynchronous funtion from the outside. You'd have to await, which results in suspension of your active thread.
If in case you'd want to make updateModel async, the code within will always be synchronous unless if you explicitly suspend it by calling await. The order of execution of multiple updateModel calls is not very deterministic. As far as you don't suspend within the updateModel block, it is sure that they execute serially. In such case, there is no use making the updateModel async.
If your update model code is synchronous you can make your model actor type to synchronize access. Actors in swift behave similar to serial DispatchQueue, they perform only one task at a time in the order of submission. However, current swift actors are re-entrant, which means if you are calling any async method actor suspends the current task until the async function completes and proceeds to process other submitted tasks.
If your update code is asynchronous, using an actor might introduce data race. To avoid this, you can wait for non-reentrant actor support in swift. Or you can try this workaround TaskQueue I have created to synchronize between asynchronous tasks or use other synchronization mechanisms that are also created by me.