CustomActor + assumeIsolated

We have static func assumeIsolated() defined on MainActor, how do I implement a similar thing on a custom global actor?

CustomActor.assumeIsolated { ... }

It seems you can replicate assumeIsolated by discarding the closure isolation through unsafeBitCast. This is what the MainActor implementation does.

@globalActor
actor MyActor {
  private init() {}

  static let shared = MyActor()

  static func assumeIsolated<T: Sendable>(_ operation: @MyActor () throws -> T) rethrows -> T {
    shared.preconditionIsolated()

    return try withoutActuallyEscaping(operation) { escapingClosure in
      try unsafeBitCast(escapingClosure, to: (() throws -> T).self)()
    }
  }
}
4 Likes

Yes, for global actors there sadly isn’t a way for the stdlib to provide this function because we would have to be able to write func test<A: Actor>(_: @T () → ()) and sadly generics don’t work with @ attributes like actor isolation like that.

If you need this function on a custom global actor, you’ll have to copy the implementation as the reply above suggests. Please do not forget the preconditionIsolated or some other way to verify this is actually safe though!

2 Likes

Thank you both, this works great indeed.

I do have a related followup question though. Consider this fragment:

typealias TheActor = MainActor

@TheActor class MyModel {
    nonisolated func foo() {
        if Thread.isMainThread {
            // synchronous path (preferred when possible)
            TheActor.assumeIsolated { 
                doSomething()
            }
        } else {
            // asynchronous path (undesired)
            Task { @TheActor in
                self.doSomething()
            }
        }
    }
    /* isolated */ func doSomething() {
        // ...
    }
}

The class is isolated to the main actor and the idea of the check is to perform the "doSomething" operation "as synchronously as possible" avoiding the extra dispatch hopping at all costs.

Now, obviously, if I change TheActor to be MyActor:

typealias TheActor = MyActor

I'd need something different than this check:

        if Thread.isMainThread { ... }

What would be the equivalent TheActor.isOnThisActor check?

I think this pattern can be solved through the new Task.immediate. In this section of the proposal the example is quite similar to yours.

The function would then look like this:

nonisolated func foo() {
  Task.immediate { @TheActor in
    doSomething()
  }
}

If foo is called from a function that has the same executor as TheActor then the body of Task.immediate will be run synchronously until the first suspension point. Otherwise, it behaves like Task.init.

3 Likes