Reputation: 18204
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?
Upvotes: 19
Views: 15235
Reputation: 646
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.
Upvotes: 24
Reputation: 437552
You said:
To my understanding, since we use the
@MainActor
the work is done on the main thread. Meaning the work onTask
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.
Upvotes: 9
Reputation: 18204
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.
Upvotes: 2