Delegates + task isolated property

Every time I think I am getting a hang of the isolation in Concurrency I run into issues like this :sweat_smile:.

I have this protocol

@MainActor
public protocol RepairBayDelegate: AnyObject {
  func repairBotAssemblyDidUpdate(progress: Double)
  func repairBotReady(_: RepairBot)
}

I then have this class that uses the delegate.

public final class RepairBay: BattleshipMessageReceiver {
  public weak var delegate: RepairBayDelegate?

  private func assembleRepairBot() async throws -> RepairBot {
    // ...
    await delegate?.repairBotAssemblyDidUpdate(progress: 0.15)
   }

Doing so will generate the following warning
"Non-sendable type '(any RepairBayDelegate)?' in implicitly asynchronous access to main actor-isolated property 'delegate' cannot cross actor boundary"

I thought "ok, that makes sense because my class using the delegate is not in the MainActor"

My solution then was to isolate the variable to the MainActor and use it in a method also isolated to the MainActor.

@MainActor
public weak var delegate: RepairBayDelegate?

  @MainActor
  private func updateProgress(_ progress: Double) {
    delegate?.repairBotAssemblyDidUpdate(progress: progress)
  }

  private func assembleRepairBot() async throws -> RepairBot {
    // ...
    await updateProgress(0.15)
   }

Now I get "Sending 'self' risks causing data races; " in the line await updateProgress(0.15).

My Solution:
I can make all of this go away if I just remove the MainActor isolation from the protocol. Maybe I was wrong adding that to the protocol definition?

Regardless, I feel confused. What would I have done if I couldn't change the protocol definition? How can I use a variable that's in another isolation boundary when self and the variable are not Sendable?

By my reading this is pretty much a logical contradiction. If a value is 'not Sendable' then it definitionally cannot be transferred to another isolation domain (outside of narrow cases where it's provably safe via region-based isolation).

Do you expect all your types conforming to RepairBayDelegate to be @MainActor themselves? Can you make the RepairBayDelegate refine Sendable if so? If not for some reason, and if RepairBay can't be made Sendable either, you'd probably need to make sure that any calls on RepairBay which need to access the delegate are themselves @MainActor.

Removing the @MainActor isolation turns the protocol requirements in to vanilla synchronous functions, which means there's no crossing of isolation domains. If this is a viable option then it's also okay, and expresses that everything stays within a single isolation domain so sendability is not required.

2 Likes

The major problem you have here is that RepairBay not Sendable (neither explicitly nor by isolation to global actor), and you have async function that now accesses its state delegate from the potentially racy function.

I have faced a distinct issue what global-actor isolated protocol is treaded as not-Sendable, to me this seems like a bug, but probably it is not, in either way — adding Sendable to the delegate here looks to be OK requirement (it is going to by such as conforming type is going to be isolated).

But you’ll still have an issue with non-isolated non-Sendable RepairBay when you’ll try to call its async methods (more likely, there are few exceptions, but usually they require careful design to make use of them).

1 Like

Thank you both from your answers. Seems like I was right to remove the MainActor isolation. I thought I was missing something.
I am still getting the hang of the isolation stuff. Don't have the right mental model yet.

1 Like