Use a `Protocol` of @MainActor instead of concrete @MainActor class produces an error

Hi everyone! Could anyone please clarify how the concurrency checking works in my case? I have an actor that calls the foo method of a @MainActor class:

actor MyActor {
    let property: MainActorClass

    init(property: MainActorClass) {
        self.property = property
    }

    func doSomething() async {
        await property.foo()
    }
}

@MainActor
class MainActorClass {
    func foo() async {
        print("foo")
    }
}

It compiles with Swift 6, but when I'm trying to abstract my MainActorClass with MainActorProtocol:

actor MyActor {
    let property: MainActorProtocol

    init(property: MainActorProtocol) {
        self.property = property
    }

    func doSomething() async {
        await property.foo()
    }
}

@MainActor
protocol MainActorProtocol {
    func foo() async
}

class MainActorClass: MainActorProtocol {
    func foo() async {
        print("foo")
    }
}

I am getting an error:


What's the difference between these two approaches and how can I abstract my MainActorClass with a Protocol?

@globalActor concrete types and @globalActor protocol are different.

concrete types

  • every thing is isolated by the global actor (except deinit and nonisolated init)
  • since global actor guarantee the isolation, it is implicitly Sendable

protocol

  • only declared property and method are isolated by the global actor
  • which means It is not Sendable

What you can do using protocol would be something like below.

@MainActor
protocol MainActorProtocol: Sendable {
    func foo() async
}

class MainActorClass: MainActorProtocol {
    func foo() async {
        print("foo")
    }
}

Now any types that conforms to MainActorProtocol is guarded by MainActor by default. Because that is the only way for default implementation to guarantee Sendable for MainActor isolated protocol.

@NeonTetra Thanks for the clarification! Also, I noticed that according to this Apple Documentation, if I have only async methods inside a Protocol I can mark @MainActor only MainActorClass and not MainActorProtocol.

Because async methods guarantee isolation by switching to the corresponding actor in the implementation.

But if I do that I am still getting the same error:

actor MyActor {
    let property: MainActorProtocol

    init(property: MainActorProtocol) {
        self.property = property
    }

    func doSomething() async {
        await property.foo() //Sending 'self.property' risks causing data races
    }
}


protocol MainActorProtocol { // actor is not specified
    func foo() async
}

@MainActor
class MainActorClass: MainActorProtocol {
    func foo() async {
        print("foo")
    }
}

well you have to remember that only actor isolation is guarantee. But this also impiles MainActorProtocol is shared between different actor isolation. Which is in other word data race.
Also any async function that doesnt have isolation hint, Concurrency tries to dispatch that async method to the nonisolate global context.

So in swift 6 you have to guarantee one of 3 to be met.

  1. Sendable : MainActorProtocol must be Sendable so that is safe to be shared across different actor isolation. ex) Locking, Global Actor, Actor etc...
  2. disconnect the non sendable : detail is in here region-based-isolation, transferring. Which means you have send the whole instance to the different region and disconnect it. and take it back when it is done
  3. do not cross the actor isolation
actor MyActor {
    var property: MainActorProtocol

    init(property: MainActorProtocol) {
        self.property = property
    }

    func doSomething() async {
// property is in the same actor isolation
        await property.foo(isolated: #isolation)
    }
}


protocol MainActorProtocol { // actor is not specified
    func foo(isolated actor: isolated (any Actor)?) async
}



@MainActor
class MainActorClass: MainActorProtocol {
    
    func foo(isolated actor: isolated (any Actor)? = #isolation) async {
        print("foo")
    }
    

}

1 Like

But what is an example of using a protocol without crossing the actor isolation?
Since we usually pass protocols values from outside, should we always use Sendable for protocols?

Sendable means it is safe to share or send between different isolation. without explicit disconnection.

in order to not across actor isolation, async function itself must have an access to the current isolation
for protocol it would be like below

protocol MainActorProtocol { 
    func foo(isolated actor: isolated (any Actor)?) async
}

Thank you for your answers!

It looks like in Swift 6 using protocols is much more complicated. Especially if you have a big dependency hierarchy. You either need to pass an isolated actor through the whole hierarchy or mark every dependency in the hierarchy as Sendable. Both options are not very handy. Much easier just to remove all protocols and use concrete types everywhere :)