SE-0327 (Second Review): On Actors and Initialization

One way to postpone the await self.click() as you describe is to sort of reserve, or lock, the actor at the start of the initializer, and then unlock it once exiting. This way, any new tasks that try to await during the initialization are only run afterwards. That idea was considered in an early pitch here (and discussed on the forums here) but I forgot to put it into the proposal. A solution like that would remove the race condition for plain actor types and would be quite nice.

Unfortunately, one of the downsides of that locking idea was that it didn't solve the problem for this kind of situation:

@MainActor
class MainActorClicker {
  var count: Int
  func click() { self.count += 1 }

  nonisolated init(bad: Void) {
    self.count = 0
    Task { await self.click() }
    self.count += 1
    print(self.count)
  }
}

Here, I've changed the actor type to instead be a MainActor-isolated class. Plus, I've added nonisolated on the init so that people can call MainActorClicker.init without an await. This in effect gives us an actor instance whose data and methods must be used while on the main thread.

The locking idea works if the operation to lock the executor is guaranteed to not block a thread. That's true for plain actor types, since the instance's executor was freshly created and is guaranteed to be idle. For the code example above it's not true, because another task may be using the MainActor when calling MainActorClicker.init. We would have to block the thread running the non-async MainActorClicker.init so it can wait for the MainActor to become free in order to reserve it during initialization. But, blocking threads goes against the design of Swift concurrency.

Imagine that, instead of trying to reserve the shared executor, we have each fresh instance of a MainActorClicker contain some special flag / lock to communicate to other tasks that the object is still being initialized, so its methods aren't safe to enter yet. In practice I do not think that is workable, since it would mean runtime tests for every method call to check if it is safe to enter, etc.

1 Like