When type is marked with a global actor, does methods inherit the actor isolation?

A mainActor function

@MainActor func doSomethingThenFollowUp(_ body: @escaping @Sendable () -> Void) {
  Task.detached {
    body()
  }
}

When marking the whole class with MainActor, it shows an error on self.clickedCount += 1

@MainActor
class MyButton: @unchecked Sendable {
  var clickedCount = 0
  
  func onClicked() { 
    doSomethingThenFollowUp {
      self.clickedCount += 1 // `main actor-isolated property 'clickedCount' can not be mutated from a Sendable` and `mutation of this property is only permitted within the actor`
    }
  }
}

But if I mark MainActor on the method of onClicked instead of the whole type, it will stop the error.

class MyButton: @unchecked Sendable {
  var clickedCount = 0
  
  @MainActor
  func onClicked() { 
    doSomethingThenFollowUp {
      self.clickedCount += 1 
    }
  }
}

As my understanding, when marking the whole type with global actor annotation, its methods become to run on the same global actor.
Am I missing something?

The closure argument to doSomethingThenFollowUp is not automatically tied to the class's isolation context. I think it can't be, because the parameter is sendable and escaping, with no particular isolation requirements, so that function is free to run the closure argument anywhere (irrespective of whether the function itself is tied to the main actor, it could pass the closure off to something which is not).

You can force your closure argument to also be tied to the main actor like:

doSomethingThenFollowUp { @MainActor in 
    self.clickedCount += 1 
}

…though you might still have to address further complaints from the compiler (i.e. doSomethingThenFollowUp needs to actually ensure the closure is only invoked from the main actor).

I was testing on swift 5.10 on Linux and enabled -strict-concurrency=complete

I still don't get what you said.

if the closure is sendable, it would expect the closure can be run across the threads? doesn't it?

It's not about the global actor annotation on onClicked, it's about var clickedCount. In your first example, clickedCount is @MainActor-isolated. In your second example, clickCount is not isolated. In both cases, the closure to doSomethingThenFollowUp is nonisolated, so it's only an error in the first case where clickCount is isolated to the main actor.

1 Like

the closure to doSomethingThenFollowUp is nonisolated??
But I marked doSomethingThenFollowUp as @MainActor

I think I see where the confusion is:

The closure argument is passed to body which has type @escaping @Sendable () -> Void. This function type is not isolated to the main actor, and in the implementation of doSomethingThenFollowUp, it's not called on the main actor -- it's called in a detached task, which is on the global concurrent thread pool, aka the "generic executor". If you annotate body as @MainActor, you would get a compiler error:

@MainActor func doSomethingThenFollowUp(_ body: @escaping @MainActor @Sendable () -> Void) {
  Task.detached {
    body() // error: Expression is 'async' but is not marked with 'await'
  }
}

got it. thanks.