Search code examples
swiftconcurrencyactor

Calling a method from an actor's init method


I'm trying to convert one of my classes in Swift to an actor. The current init method of my class calls another instance method to do a bunch of initialization work. After converting, here's a simplified version of my actor:

actor MyClass {
    private let name: String

    init(name: String) {
        self.name = name

        self.initialize()  // Error on this line
    }

    private func initialize() {
        // Do some work
    }

    func login() {
        self.initialize()
        // Do some work
    }

    // Bunch of other methods
}

I get the following error when I try to compile:

Actor-isolated instance method 'initialize()' can not be referenced from a non-isolated context; this is an error in Swift 6

I found that I can replace self.initialize() with:

Task { await self.initialize() }

Is that the best way to do this? Could this cause any race conditions where an external caller can execute a method on my actor before the initialize() method has a chance to run? Seems cumbersome that you are not in the isolated context in the actor's init method. I wasn't able to find any Swift documentation that explained this.


Solution

  • I wasn't able to find any Swift documentation that explained this.

    This is explained in the proposal SE-0327, in this section (emphasis mine):

    An actor's executor serves as the arbiter for race-free access to the actor's stored properties, analogous to a lock. A task can access an actor's isolated state if it is running on the actor's executor. The process of gaining access to an executor can only be done asynchronously from a task, as blocking a thread to wait for access is against the ethos of Swift Concurrency. This is why invoking a non-async method of an actor instance, from outside of the actor's isolation domain, requires an await to mark the possible suspension. The process of gaining access to an actor's executor will be referred to as "hopping" onto the executor throughout this proposal.

    Non-async initializers and all deinitializers of an actor cannot hop to an actor's executor, which would protect its state from concurrent access by other tasks. Without performing a hop, a race between a new task and the code appearing in an init can happen:

    actor Clicker {
      var count: Int
      func click() { self.count += 1 }
    
      init(bad: Void) {
        self.count = 0
        // no actor hop happens, because non-async init.
    
        Task { await self.click() }
    
        self.click() // 💥 this mutation races with the task!
    
        print(self.count) // 💥 Can print 1 or 2!
      }
    }
    

    To prevent the race above in init(bad:), Swift 5.5 imposed a restriction on what can be done with self in a non-async initializer. In particular, having self escape in a closure capture, or be passed (implicitly) in the method call to click, triggers a warning that such uses of self will be an error in Swift 6.

    They then went on to give another example that should have been accepted by the compiler, and suggested to loosen those restriction. In either case though, it does not apply to your code. See this section if you want the details about why it still does not compile in newer versions of Swift.

    So the solution is, as the first quote says, mark the init as async. This way the initialiser would be effectively run on the actor's executor. The caller would use await when initialising, to "hop" in.