How Actor can Conform to protocol and mutate its internal state in nonisolated method that is required by this protocol. There is no write access in nonisolated method to isolated property by actor.
protocol P : NSObjectProtocol, Sendable {
func mutateState()
}
actor A: NSObject, P {
var number: Int = 0
nonisolated func mutateState() {
number += 1 // <- Error: Actor-isolated property 'number' can not be mutated from a nonisolated context
}
}
I was trying to use queue to synchronize access like this:
actor A: NSObject, P {
private let queue = DispatchSerialQueue(label: "example", qos: .utility)
nonisolated var unownedExecutor: UnownedSerialExecutor {
queue.asUnownedSerialExecutor()
}
var number: Int = 0
nonisolated func mutateState() {
queue.async { [self] in
number += 1 // <- Actor-isolated property 'number' can not be mutated from a Sendable closure
}
}
}
I know last example is correct from data race point of view but compiler seems to don't know this. So my question is how to do this correctly co Compiler will be happy. In the project we have SWIFT_STRICT_CONCURRENCY
configured to Complete
. The P
protocol is not owned by us and we can't modify this. Of course this is some extracted example in the simples form just to nail problem. The real protocol we try to implement is AVAssetWriterDelegate
from AVFoundation
. When we try to replace actor to final class then another problem: Stored property 'number' of 'Sendable'-conforming class 'A' is mutable
.
Any guide how to handle such cases?
If you are fine with asynchronously incrementing number
, then you can just wrap it in a Task
,
nonisolated func mutateState() {
Task {
await incrementNumber()
}
}
func incrementNumber() {
number += 1
}
Note that await number += 1
is not allowed by design, because it is syntactically unclear when the actor hop happens.
If the increment needs to be done synchronously, you need to write a Sendable
or @unchecked Sendable
class, with synchronisation mechanisms. For example:
import Synchronization
final class A: NSObject, P {
let number = Mutex<Int>(0)
func mutateState() {
number.withLock {
$0 += 1
}
}
}
Side note: to convince Swift that the code will be isolated to self
at runtime, you can use assumeIsolated
.
queue.async {
self.assumeIsolated {
$0.number += 1
}
}
But obviously in this case you can just create a Task
and don't need to use your own queue.