Reputation: 2272
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.
Upvotes: 6
Views: 8079
Reputation: 274500
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
.
Upvotes: 6
Reputation: 110
Why is it possible for actor protocol to have non-async methods declared, without an explicit nonisolated keyword?
Actor
methods are inherently synchronous. However those synchronous methods can be part of some broader async
routine. Actor
s isolate their methods in order to protect their shared mutable state from being accessed by some other concurrent routines.
Mutation can occur only from an isolated method within an Actor
. Isolated methods cannot be called from a synchronous context outside of an Actor
.
actor MyActor {
var state = 0
func foo() {
state += 1
}
}
let actor = MyActor()
actor.foo() // Error: Call to actor-isolated instance method 'foo()' in a synchronous nonisolated context
If you want to call a method from a synchronous context outside of an Actor
, you need to make it nonisolated
. But this will imply no mutation, as Actor
s are meant to protect their state.
actor MyActor {
var state = 0
nonisolated func foo() {
state += 1 // Error: Actor-isolated property 'state' can not be mutated from a non-isolated context
}
}
let actor = MyActor()
actor.foo()
Nonisolated
work only if we won't mutate Actor
's state
actor MyActor {
nonisolated func foo() {
print("I'm not mutating here..")
}
}
let actor = MyActor()
actor.foo()
To sum up:
Methods in Actors are not inherently async, but if you want to call them from outside and still respect isolation rules, it needs to be done from an asynchronous context. On the other hand, nonisolated
methods must be immutable, so they respect isolation rules by design and they can be safely called from a synchronous context.
Let's say it's possible to have non-async methods, then why would I ever have Error-1 in my code?
Because MyImplementation1Proxy.foo()
is synchronous and you are calling an actor-isolated method MyImplementation1.foo()
there, thus not respecting MyImplementation1
isolation rules.
EDIT 1:
Given 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
MyImplementation1.foo
is not async, as I mentioned "Actor methods are inherently synchronous". And so MyImplementation1Proxy.foo
is synchronous. But they need to be called from an asynchronous context anyway to respect their isolation rules, unless they are nonisolated
. I know, it's a bit tricky.
If you want to use await
inside of those methods, it's clear that you need to explicitly make them asynchronous, because they are not. Thus Error-2
Upvotes: 1