When I mark a protocol with async functions with @MainActor and it's conformance does not specify it's functions are async, it is not executing on main thread. I don't know if this is a bug or not.
For example, given the following protocol and implementation:
@MainActor
protocol MyMainActorProtocol {
func doStuff() async
}
struct MyMainActorStruct: MyMainActorProtocol {
nonisolated init() {}
func doStuff() {
print(">>> \(Thread.isMainThread)")
}
}
When I execute this code:
Task {
let dependency: MyMainActorProtocol = MyMainActorStruct()
print(">>> \(Thread.isMainThread)")
await dependency.doStuff()
}
both print statements run in a background thread.
But with the following adjustments of either
@MainActor
protocol MyMainActorProtocol {
func doStuff()
}
struct MyMainActorStruct: MyMainActorProtocol {
nonisolated init() {}
func doStuff() {
print(">>> \(Thread.isMainThread)")
}
}
or
@MainActor
protocol MyMainActorProtocol {
func doStuff() async
}
struct MyMainActorStruct: MyMainActorProtocol {
nonisolated init() {}
func doStuff() async {
print(">>> \(Thread.isMainThread)")
}
}
I get the correct result, where the body of the function inside the struct runs in the main thread.
Is this expected behavior? What is the explanation behind it.
According to the global actor inference rules,
A non-actor type that conforms to a global-actor-qualified protocol within the same source file as its primary definition infers actor isolation from that protocol
So provided you put them in the same source file, MyMainActorStruct
should be MainActor
-isolated too, and so is doStuff
.
As a result, you don't get errors when you try to e.g. initialise a UIView
in doStuff
. However, when you do try to do that at runtime, you get logs in the console saying "UI API called on a background thread". This should not happen. This behaviour is clearly a bug.
Note that even if you add @MainActor
to MyMainActorStruct
, or even to doStuff
, this behaviour still remains.
To find out how this happened, I compared the SIL generated by the following codes (See the full SIL on godbolt.org):
@MainActor
protocol MyMainActorProtocol {
func doStuff() async
}
@MainActor
struct MyMainActorStruct: MyMainActorProtocol {
nonisolated init() {}
func doStuff() async { }
}
@MainActor
protocol MyMainActorProtocol {
func doStuff() async
}
@MainActor
struct MyMainActorStruct: MyMainActorProtocol {
nonisolated init() {}
func doStuff() { }
}
The former has a hop_to_executor %4 : $MainActor
line in doStuff
, while the latter lacks it.
This is very similar to a previous (fixed) bug that happens in a very similar situation. That bug was about actor
s implementing async
protocol requirements with non-async
code, e.g.
protocol Foo {
func foo() async
}
actor FooActor : Foo {
func foo() { }
}
and the same actor-hop was missing as well. Although this bug was fixed, it could very likely be that they also forgot to emit an actor hop in the case of global actors.