I'm trying to define a struct which has a mutating function which is also async, for use inside an actor or a type isolated to a global actor. I'm running into the following error:
Cannot call mutating async function 'foo' on actor-isolated property 'bar'
I understand this is because we need to somehow guarantee that mutations are isolated to the correct actor. Is there a way I can have the mutating method inherit the isolation of the containing type?
For context, I'm trying to implement something like this to deal with actor reentrancy:
struct AsyncSerialQueue {
mutating func execute<T: Sendable>(_ body: () async throws -> T) async rethrows -> T {
if isExecuting {
let (stream, continuation) = AsyncStream<Never>.makeStream()
continuations.append(continuation)
for await _ in stream {
}
} else {
isExecuting = true
}
defer {
if let next = continuations.first {
continuations.removeFirst()
next.finish()
} else {
isExecuting = false
}
}
return try await body()
}
private var isExecuting: Bool = false
private var continuations: [AsyncStream<Never>.Continuation] = []
}
Hmm I've tried some variations of this and am still unable to get this to work... I haven't tried with the as-yet-unapproved closure isolation control yet which may be the missing piece. In the meantime, I just pasted the code directly into the body of the actor, which isn't great but does work.
I have had doubts that mutating might affect this and seems that's the case: compiler cannot reason that it is safe to call mutating async method here, because such method can be unsafe in general (see this thread). Simpler case:
struct Value {
private(set) var wrappedValue: Int?
mutating func load() async {
wrappedValue = 1
}
}
actor Demo {
private var value = Value()
func test() async {
await value.load()
}
}
I thought that adding isolated parameter should fix that as now compiler can reason that mutation happens within the same context, but either I'm wrong in this assumption or this is a bug. I think I was wrong: because of re-entrancy, which you are trying to handle in the first place, test in my example can be invoked several times while awaiting, violating exclusivity law with or without isolation. So the struct is simply wrong tool for the job.
EDIT. You can try utilize custom executor with actor AsyncSerialQueue so that it won't be introducing additional hops:
actor AsyncSerialQueue {
nonisolated var unownedExecutor: UnownedSerialExecutor {
parent.unownedExecutor
}
private nonisolated unowned let parent: any Actor
init(parent: any Actor) {
self.parent = parent
}
func execute<T: Sendable>(_ body: () async throws -> T) async rethrows -> T {
}
}