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
@MainActorthe work is done on the main thread. Meaning the work onTaskwould 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With