I have a crash that looks like it's deep inside a swift RefCount, it makes very little sense, and is not descriptive at all.
DispatchQueue.sync(barrier: true) should make the following code thread safe. So not sure what the crash is and how it happens.
It's only occurred twice in a month of testing.
Here is the code :
var queue: DispatchQueue = DispatchQueue(label: "global_state", attributes: .concurrent)
struct GlobalState: Equatable, Hashable {
var dataStateXX ... // This is always an equatable/hashable value type
}
var queue: DispatchQueue = DispatchQueue(label: "global_state", attributes: .concurrent)
fileprivate var unsafeState: GlobalState = .init()
var state: GlobalState {
set {
// perform writes on data synchronously to keep the data thread safe
let threadValue = newValue
queue.sync(flags: .barrier) {
self.unsafeState = threadValue
DispatchQueue.main.async {
self.stateSubject
.send(self.unsafeState)
}
}
}
get {
unsafeState
}
}
private let stateSubject: CurrentValueSubject<GlobalState, Never>
Here is the error : #8 (null) in bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) ()
Thanks for the suggestions. It doesn't really explain the issue however, since you can read memory while writing to a block from different threads. But I can see that forcing it onto the same thread for the read and write would be the safest option.
In terms of optimisation, I'm not too worried, if I wanted an optimised system, I wouldn't be touching either SwiftUI or Combine. :)
But what you read may be gibberish because it may be a random mix of multiple versions of the value; you might read the value when it's only partly updated during a set. Mutations are only implicitly atomic if the data structure in question is quite small (typically machine-word size, so 64-bit on most current platforms), and even then only if fully aligned (usually but not always the case for objects and values), among other prerequisites.
Reading an owned value includes retaining it. It is not an atomic operation in Swift. After that, all bets are off where you’re going to crash. (If you run under TSan, it should tell you this, but I haven’t actually tested it.)
This is something people forget because the corresponding code in Objective-C is thread-safe…for atomic properties. But there’s nothing clever going on there; atomic ObjC properties have a lightweight lock in case there’s actually a conflict. For better or worse, Swift doesn’t provide that for you.
In that case, I would expect the following approach would solve it ->
let stateLock = NSLock()
fileprivate var unsafeState: GlobalState = .init()
var state: GlobalState {
set {
// perform writes on data synchronously to keep the data thread safe
stateLock.withLock {
self.unsafeState = newValue
}
DispatchQueue.main.async {
self.stateSubject
.send(newValue)
}
}
get {
let safeState: GlobalState
stateLock.lock()
safeState = unsafeState
stateLock.unlock()
return safeState
}
Yes, I'd do this. Getter could be further simplified to a shorter:
get {
stateLock.withLock {
unsafeState
}
}
Two things to note about this implementation:
you'll be posting main thread updates even if the state hasn't been actually changed (assigned to the same value). A mitigation her could be making the state equatable and comparing it, although that would add processing time, especially if there are many duplicate updates.
you'd be posting multiple main thread updates when you change state frequently (e.g. in a for i in 0 ... 1000 loop), in other words there's no "don't spam main thread too often" throttling. A mitigation here could be implementing a throttling to not update main thread too often (say, not faster than 120 times per second).