I have an actor protocol declared like this:
protocol MyActorProtocol: Actor {
func foo()
}
There's an actor which conforms to the protocol:
actor MyImplementation1: MyActorProtocol {
func foo() {}
}
Now I need to add a proxy:
actor MyImplementation1Proxy: MyActorProtocol {
let impl: MyActorProtocol
init(impl: MyActorProtocol) {
self.impl = impl
}
func foo() {
// Error-1: Call to actor-isolated instance method 'foo()' in a synchronous actor-isolated context
// impl.foo()
// Error-2. 'await' in a function that does not support concurrency
// await impl.foo()
// Success-3. only this passes the compiler check
Task { await impl.foo() }
}
}
I want to understand such points:
nonisolated
keyword?Error-1
in my code?MyImplementation1Proxy
also conforms to MyActorProtocol
, and MyImplementation1.foo
must be called in Task
(for whatever reason), then it feels MyImplementation1.foo
is "kind of async", so MyImplementation1Proxy.foo
should have this "kind of async context" as well, so why do I have Error-2
?Error-2
looks like the method is just "non async", but when I tried to introduce a non-actor implementation, got Call to actor-isolated instance method 'foo()' in a synchronous nonisolated context
, which is fair but again leads to question 1:class MyImplementation2 {
let impl: MyActorProtocol
init(impl: MyActorProtocol) {
self.impl = impl
}
func bar() {
impl.foo()
}
}
Thanks in advance.
You are conflating isolated vs non-isolated and async vs sync, which are orthogonal distinctions.
A method isolated to some actor instance can be synchronously called from a context that is isolated to that actor instance. Otherwise, it has to be called asynchronously, i.e. with await
. Calling an isolated method from a context not isolated to that actor also involves an "actor hop", which has some requirements about Sendable
, but I digress.
An async
method must be called asynchronously everywhere. You can only use await
in the body of an async
method.
You can declare all four combinations of these in an actor, and there is no reason why any of these combinations can't be required by a protocol.
actor Foo {
func f1() { // non-async isolated
}
nonisolated func f2() { // non-async non-isolated
}
func f3() async { // async isolated
}
nonisolated func f4() async { // nonisolated async
}
}
You cannot do await impl.foo()
simply because foo
is not async (though it is isolated).
impl.foo()
is not valid either, because impl.foo
is isolated to impl
, but you are doing this call from a context that is isolated to self
(the proxy). These are different actors. Note that this is not just because their types are different, but because self
and impl
are different instances. This is the same reason as why you cannot do something like this:
actor Foo {
func foo() {
}
func bar() {
// Foo().foo() is isolated to the new instance of Foo, not self!
Foo().foo()
}
}
To make this proxy work, you have to convince Swift that once execution is isolated to self
(the MyImplementation1Proxy
), it is also isolated to impl
.
You should implement unownedExecutor
to return that of impl
's. Then, wrap the call with assumeIsolated
.
actor MyImplementation1Proxy: MyActorProtocol {
let impl: MyActorProtocol
nonisolated var unownedExecutor: UnownedSerialExecutor {
impl.unownedExecutor
}
init(impl: MyActorProtocol) {
self.impl = impl
}
func foo() {
impl.assumeIsolated {
$0.foo()
}
}
}
When someone calls MyImplementation1Proxy.foo
, the call is executed by MyImplementation1Proxy.unownedExecutor
, which happens to be the same executor as whatever its impl
is using. assumeIsolated
checks that indeed, the current executor is impl.unownedExecutor
, and runs foo
.