Swift non actor isolated closures

Do escaping closure that are passed to actor method inherit actor isolation? Or they are non isolated?

For instance:

actor MyActor {
   func method(_ closure: @escaping () async -> Void) {
      await closure()
   }
}

With what isolation will closure be created? In my simple test, seems that closure inherit it's context isolation on allocation

actor MyActor {
   func method(_ closure: @escaping () async -> Void) async {
       print("in actor method: ", Thread.current)
       await closure()
       print("in actor method: ", Thread.current)
   }
}

func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        let actor = MyActor()
        Task {
            print("in task closure: ", Thread.current)
            await actor.method {
                print("in actor closure: ", Thread.current)
                try? await Task.sleep(nanoseconds: 1_000_000_000)
                print("in actor closure: ", Thread.current)
            }
            print("in task closure: ", Thread.current)
        }
        
        return true
}

Outputs:

in task closure:  <_NSMainThread: 0x283604380>{number = 1, name = main}
in actor method:  <NSThread: 0x283654400>{number = 5, name = (null)}
in actor closure:  <_NSMainThread: 0x283604380>{number = 1, name = main}
in actor closure:  <_NSMainThread: 0x283604380>{number = 1, name = main}
in actor method:  <NSThread: 0x283654400>{number = 5, name = (null)}
in task closure:  <_NSMainThread: 0x283604380>{number = 1, name = main}

I know that it's it proper proof of hypothesis, therefore I'm asking: Is there are any proposals or statements, which describe what isolation async closure do get?

You can't equate "what thread something runs on" to "is it isolated to the same actor". Threads are re-used, and being on the same doesn't mean that you'll always be. We don't have a way to assert "am I on the actor I'm expecting" sadly, it'd be a nice API addition IMHO.

To answer your question though: No, you're not isolated to the same actor. There's nothing semantically connecting the passed closure to the actor in there.

If you wanted to make it such that the closure always runs on the actor it was passed to you could write it as:

actor MyActor {
   func method(_ closure: @escaping (isolated MyActor) async -> Void) async {
      await closure(self)
   }
}

The isolated MyActor ensures that the closure is isolated to the specific actor instance passed into there. And the only way to hand out isolated actor refs, is getting one from the actor itself -- i.e. the self here.

I guess it should also work with isolated Actor if you wanted to not expose the concrete actor type/instance.

6 Likes

As I understood you, we should just pass actor itself as closure parameter.
Hm, but why does it add some isolation?

Btw, someone told me that closures inherit it context isolation. And referred my to proposal that says:

A closure formed within an actor-isolated context is actor-isolated if it is non-@Sendable, and non-isolated if it is @Sendable. For the examples above:

The closure passed to detach is non-isolated because that function requires a @Sendable function to be passed to it.
The closure passed to forEach is actor-isolated to self because it takes a non-@Sendable function.

Hm yeah that's also right; It's somewhat implicitly if it can't "escape the actor" (non-@Sendable) then it basically is isolated to it indeed.

1 Like

If it's non-@Sendable then you can't pass it to the actor in the first place unless you're already there.

1 Like