iosswiftconcurrency

Why can I access isolated ivars in actor's init in Swift


I have this code:

actor Actor {
  
  @MainActor
  var checker: @MainActor @Sendable () -> Void = {}
  
  init(checker: @escaping @Sendable () -> Void) {
    self.checker = checker
    _ = self.checker // access it in nonisolated context
  }
  
  nonisolated func noniso() {
    Task { @MainActor in
      _ = self.checker
    }
  }
}

My understanding is that, the init is nonisolated, because I can create the actor in nonisolated context:

let actor = Actor {}

However, in the nonisolated function noniso, I have to use a @MainActor in order to access the self.checker. Otherwise there will be compiler error.

Why can I access this isolated ivar checker in this nonisolated context in the init?

Please do not judge why I put a @MainActor ivar in an actor, etc. This is just experimental code for me to learn swift concurrency.


Solution

  • Non-async initialisers in an actor are indeed not isolated to the actor, because of their synchronous nature - an actor hop cannot be done.

    However, the compiler can prove that accessing self.checker is safe in the initialiser. If two threads call init at the same time, then there will just be two actor instances created, each with their own checker.

    During the execution of the initialiser, no other thread can access self (the object that is being initialised) either, unless the initialiser passed self to somewhere else.

    Since checker is main actor isolated, a good example of this is to try to pass self to a @MainActor function.

    actor SomeActor {
        
        @MainActor
        var checker: @MainActor @Sendable () -> Void = {}
        
        init(checker: @escaping @Sendable () -> Void) {
            self.checker = checker
            foo(self) // error!
            self.checker = { /* something */ }
        }
    }
    
    
    @MainActor
    func foo(_ a: SomeActor) {
        // now foo has access to 'self.checker' too!
        a.checker = { /* something else */ }
    }
    

    Here the compiler correctly gives you an error. If the compiler had allowed this, self.checker = { /* something */ } and a.checker = { /* something else */ } would potentially run on different threads, and god knows what gets written to checker in the end.

    Another example is capturing self in a Task running on the main actor.

    init(checker: @escaping @Sendable () -> Void) {
        self.checker = checker
        // comment out the below line and the error is gone
        Task { @MainActor in self.checker = { /* something */ } }
        print(self.checker) // error here
    }
    

    Similarly, it is not safe to call any isolated methods on self in init. Those methods may start new Tasks that would race with whatever init is doing. See the link for more details.

    For more info on how Swift determines whether self has escaped, see the "initialisers with nonisolated self" section in SE-0327.