This is sort of a follow-up to my earlier asyncDetached falling back into main thread after MainActor call.
Here's the complete code of an iOS view controller:
import UIKit
func test1() {
print("test1", Thread.isMainThread) // true
Task {
print("test1 task", Thread.isMainThread) // false
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
test1()
test2()
}
func test2() {
print("test2", Thread.isMainThread) // true
Task {
print("test2 task", Thread.isMainThread) // true
}
}
}
The two functions test1
and test2
are identical, and are being called from the very same place. Yet one of them runs its Task initializer operation:
function on a background thread, and the other runs on the main thread.
What determines this? I can only think it has to do with where the method is declared. But what does it have to do with where the method is declared?
I think the rule must be that a Task initializer in a MainActor method runs on the main thread.
And all methods of a view controller are MainActor methods by default; plus, I observe that if I declare test2
to be nonisolated
, its Task operation runs on a background thread instead of the main thread.
My guess, then, is that this is an example of the rule that a Task initializer's operation "inherits" from its context:
test2
is a MainActor method; it runs on the main thread, so the Task operation "inherits" that.
But test1
is not marked for any special thread. test1
itself runs on the main thread, because it is called on the main thread; but it is not marked to run on the main thread. Therefore its Task operation falls back to running on a background thread.
That's my theory, anyway, But I find it curious that this rule is nowhere clearly enunciated in the relevant WWDC videos.
Moreover, even test2
is only a MainActor method in a sort of "weak" way. If it were really a MainActor method, you could not be able to call it from a background thread without await
. But you can, as this version of the code shows:
func test1() {
print("test1", Thread.isMainThread) // true
Task {
print("test1 task", Thread.isMainThread) // false
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
test1()
Task.detached {
self.test2()
}
}
func test2() {
print("test2", Thread.isMainThread) // false
Task {
print("test2 task", Thread.isMainThread) // true
}
}
}
I find that truly weird, and I have some difficulty enunciating what rule would govern this relentless context-switching behavior, so I don't regard the matter as settled.
This is because of how actor isolation and task creation work in swift. Actors have serial executor that processes one task at a time to synchronize mutable state. So any method that is isolated to the actor will run on the actor's executor. And when creating Task
with Task.init
the newly created task inherits the actor's context (isolated to the parent actor) it was created in (unless you specify a different global actor explicitly when creating task) and then processed by the actor's executor.
What's happening here is your ViewController
class and all its methods and properties are MainActor
isolated since UIViewController
is MainActor
isolated and you are inheriting from it. So your test2
method is isolated to MainActor
and when you are creating a task inside test2
the new task inherits the MainActor
context and gets executed by MainActor
on the main thread.
But this behaves differently from your test1
method because your test1
method isn't isolated to MainActor
. When you are calling test1
from viewDidLoad
the synchronous part of test1
is executed on MainActor
as part of the current task but when you are creating a new task is test1
, since test1
isn't isolated to MainActor
, your new task isn't executed on it.
To have the same behavior in test1
as test2
, you can mark your method to be isolated to MainActor
by applying the @MainActor
attribute to function definition:
@MainActor
func test1() {
print("test1", Thread.isMainThread) // true
Task {
print("test1 task", Thread.isMainThread) // true
}
}
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