I have the following code example
@globalActor
actor GlobalActor {
static let shared = GlobalActor()
}
@globalActor
actor ControllerActor {
static let shared = ControllerActor()
}
@GlobalActor
protocol MyProtocol {
var someValue: Int { get }
func doSomething() async
}
class MyClass: MyProtocol {
var someValue: Int = 0
func doSomething() async {
print("Hello")
}
}
@ControllerActor
protocol Controller {
var myProtocol: MyProtocol? { get }
}
extension Controller {
func doControllerWork() async {
await myProtocol?.doSomething()
}
}
MyProtocol
is isolated by GlobalActor
, while the Controller
protocol is isolated by ControllerActor
. When I try to call await myProtocol?.doSomething()
, I get the following error:
Sending global actor 'ControllerActor'-isolated value of type 'any MyProtocol' with later accesses to global actor 'GlobalActor'-isolated context risks causing data races.
This error disappears if doControllerWork
is implemented within a class that conforms to the Controller
protocol. However, in my scenario, doControllerWork
is shared across multiple controllers implementations.
The issue is also resolved if I use the same actor isolation for both MyProtocol
and Controller
. Unfortunately, they have very different scopes within my project, so I would like to keep them isolated under separate actors.
Let me write out your code a bit more explicitly:
if let p = myProtocol {
await p.doSomething()
}
There are three important facts here:
p
is a reference that exists in the ControllerActor contextp.doSomething()
implicitly passes p
to doSomething
as self
.doSomething
executes in the GlobalActor context.This means that p
is transferred between contexts. It therefore must be Sendable. The good news is that this is already true, since all MyProtocol-conforming types are isolated to a global actor. Swift really should be able to infer this automatically, but currently cannot.
The fix is to explicitly require Sendable:
@GlobalActor
protocol MyProtocol: Sendable { ... }
This does not require any changes to conforming types since they already are Sendable. It just lets the compiler know this is true.