Search code examples
swiftasync-awaitconcurrencyactor

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?


Solution

  • Yes, the closure will use the same actor context from which it was formed (the main actor in this example):

    As SE-0306 says

    A closure that is not @Sendable cannot escape the concurrency domain in which it was formed. Therefore, such a closure will be actor-isolated if it is formed within an actor-isolated context.


    If you do not want the closure running on the actor from which it is formed, you have a few options:

    1. You can make the closure @Sendable. As SE-0306 says:

      Actors [specify] that a @Sendable closure (described in Sendable and @Sendable closures, and used in the definition of detach in the Structured Concurrency proposal) is always non-isolated.

      Thus,

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

      That results in the closure being non-isolated (i.e., not running on the actor where the closure was created).

    2. You can also explicitly specify the closure’s actor. You could specify a global actor:

      @globalActor
      public struct SomeGlobalActor {
          public actor SomeOtherActor { }
      
          public static let shared = SomeOtherActor()
      }
      
      actor MyActor {
          func method(_ closure: @escaping @SomeGlobalActor () async -> Void) async {
              ...
          }
      }