My app uses a class A
and its subclasses SubA_x
.
A
has a static property serialNumber
that is only modified by class A
when a subclass is initialized.
It sets a property let name
so that every subclass has a unique name.
Here is the code:
class A {
static var serialNumber = 0
let name: String
init( /* some parameters */ ) {
A.serialNumber += 1
self.name = "\(A.serialNumber)"
}
}
final class SubA_1: A {
init( /* some parameters */ ) {
super.init( /* some parameters */ )
}
}
With Swift 6 strict concurrency checking, the line where serialNumber
is initialized gives the error
Static property 'serialNumber' is not concurrency-safe because it is non-isolated global shared mutable state
I understand that every subclass of A
could modify var serialNumber
from any thread, so data races are possible.
But how do I achieve this functionality in a concurrency-safe way?
I tried to provide serialNumber
by an actor:
actor SerialNumberManager {
private var serialNumber = 0
func getNextSerialNumber() -> Int {
serialNumber += 1
return serialNumber
}
}
but then I cannot call getNextSerialNumber()
in init
, except when init
is async.
But then I can initialize a subclass only in an async context, etc.
I could probably provide my own synchronization based on GCD, but there should be a way to do it within Swift concurrency.
If you want a thread-safe, shared state, an actor
is one logical approach. If you want one that you can invoke from synchronous contexts, though, you can just write your own manager, implementing your own manual synchronization (either with GCD serial queue or, as shown below, with a lock):
class SerialNumberManager: @unchecked Sendable {
static let shared = SerialNumberManager()
private let lock = NSLock()
private var serialNumber = 0
private init() { }
func nextSerialNumber() -> Int {
lock.withLock {
serialNumber += 1
return serialNumber
}
}
}
Note, we would only use the @unchecked Sendable
when we have implemented the manual synchronization, like above.
And, you could use it like so:
class A {
let serialNumber = SerialNumberManager.shared.nextSerialNumber()
let name: String
init( /* some parameters */ ) {
self.name = "\(serialNumber)"
}
}
Alternatively, you could use an OSAllocatedUnfairLock
, which already is Sendable
:
import os.lock
class A {
private static let serialNumber = OSAllocatedUnfairLock(initialState: 0)
let name: String
init( /* some parameters */ ) {
let value = Self.serialNumber.withLock { value in
value += 1
return value
}
self.name = "\(value)"
}
}
For the sake of completeness, other alternatives include atomics or UUIDs.