I understand that all the parallel work inside the actor somehow changes to serial as a part of some synchronization process. We can see that the async let-work that should be done in parallel is done sequentially in the Actor1, most likely due to the internal synchronization of the actor. But, withTaskGroup-work runs in parallel despite AnActor internal synchronization, but WHY?)
Edit: At the same time, I want to say that I understand how synchronization works when called from outside the internals of an actor when using await, but I don’t understand how synchronization works inside an actor, to call asynchronous parallel tasks inside an actor.
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView().task {
//await AnActor().performAsyncTasks() // uncomment this alternately and run
//await Actor1().performAsyncTasks() // uncomment this alternately and run
}
}
}
}
actor Actor1 {
func performAsyncTasks() async {
async let _ = asyncTasks1() // this not running in parallel
async let _ = asyncTasks2() // this not running in parallel
}
func asyncTasks1() async {
for i in 1...10_000_0 {
print("In Task 1: (i)")
}
}
func asyncTasks2() async {
for i in 1...10_000_0 {
print("In Task 2: (i)")
}
}
} // the printed text are in series with Task 1 and Task 2 in console
actor AnActor {
var value = 0
func performAsyncTasks() async {
value = await withTaskGroup(of: Int.self) { group in
group.addTask { // this running in parallel, why?!
var value1 = 0
for _ in 1...10_000 {
print("Task1")
value1 += 1
}
return value1
}
group.addTask { // this running in parallel, why?!
var value2 = 0
for _ in 1...10_000 {
value2 += 1
print("Task2")
}
return value2
}
return await group.reduce(0, +)
}
print(value)
}
} // the printed text are mixed with Task 1 and Task 2 in console
2
Answers
Consider your first example:
You said:
Yes, generally
async let
can let routines run in concurrently. But that won’t happen here only because these two functions are both isolated to the same actor, and there are noawait
suspension points.Someone said:
It is not. See SE-0317.
If you want to see parallel execution, you can use
async let
. You just need to use non-isolated functions:If I profile that in Instruments, I can see they run in parallel:
By the way, the above uses the following POI utility function:
The behaviour you are seeing is due to the nature of the async task you are performing when that task is bound to an
Actor
.The underlying thread execution model in iOS does not allow for pre-emption. That is, the CPU is never "taken away" from a task. When a task relinquishes the CPU then there is an opportunity for some other task to begin executing.
This code:
is CPU bound – There is no opportunity for any other task to run on the Actor until the
for
loop completes.Async/Await is normally used where there is some asynchronous operation; A network operation, for example.
If we make a small change to your functions:
you will see the output of the two functions intermingled because each function is relinquishing the CPU after each
print
, allowing the actor to execute the other function until it relinquishes the CPU.Now, as for why you see a different behaviour with
withTaskGroup
– This is because the task group is not bound to theActor
, even though you are creating it in a function that is bound to anActor
. A task group can use multiple threads to perform tasks. That is its primary function to allow a series of independent operations to execute with a simple rendezvous when they are all completed (or cancelled).If you remove the
await sleep
that was added and make a small change to your task group code:You will now see that the two loops complete sequentially because they are bound to the
Actor1
instance.