Hi, I am running into an use-case where I am seeing different behaviour if I add global actor isolation explicitly to group.addTask. I have declared an actor with custom executor like this:
class SerialDispatchExecutor: SerialExecutor, @unchecked Sendable {
let queue: DispatchQueue
init(label: String) {
self.queue = DispatchQueue(label: label, target: .global())
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
func enqueue(_ job: UnownedJob) {
queue.async {
job.runSynchronously(on: self.asUnownedSerialExecutor())
}
}
}
@globalActor
actor SomeActor: GlobalActor, Actor {
static let shared = SomeActor(id: "global")
@SomeActor
static var instances: [String: SomeActor] = [:]
@SomeActor
static func addInstance(_ act: SomeActor, id: String) {
instances[id] = act
}
var counter = 0
nonisolated(unsafe) var unsafeCounter = 0
let executor: SerialDispatchExecutor
init(id: String) {
self.executor = SerialDispatchExecutor(label: "my \(id)")
}
nonisolated var unownedExecutor: UnownedSerialExecutor { executor.asUnownedSerialExecutor() }
func log(external: Int) {
if counter != external {
print("Counter \(counter) with \(external)")
}
counter += 1
}
}
I have following code that tests the behaviour of actor:
await SomeActor.addInstance(SomeActor(id: "1"), id: "1")
return await withTaskGroup { group in
for index in 0..<1000 {
let task = Task(priority: (index % 2 == 0) ? .high : .low) { @SomeActor in
await withTaskGroup { group in
for (_,instance) in SomeActor.instances {
group.addTask {
await instance.log(external: index)
}
}
}
}
group.addTask {
await task.value
}
}
}
This code prints the output out of order whereas with the DispatchQueue executor I should get output in order. But the output gets fixed once I change this to:
await SomeActor.addInstance(SomeActor(id: "1"), id: "1")
return await withTaskGroup { group in
for index in 0..<1000 {
let task = Task(priority: (index % 2 == 0) ? .high : .low) { @SomeActor in
await withTaskGroup { group in
for (_,instance) in SomeActor.instances {
group.addTask { @SomeActor in
await instance.log(external: index)
}
}
}
}
group.addTask {
await task.value
}
}
}
Just adding @SomeActor isolation to group.addTask fixes this. From what I understand the closure accepted by group.addTask is marked with @isolated(any) which makes it to inherit actor context from parent. But this should have the same behaviour whether @SomeActor isolation is added explicitly or not. Can anyone explain why this difference?