In answer to your question, the lock approach suffers the exact same problems that the GCD approach does. Atomic accessor methods simply are insufficient to ensure broader thread-safety.
The issue is, as discussed elsewhere, that the innocuous +=
operator is retrieving the value via the getter, incrementing that value, and storing that new value via the setter. To achieve thread-safety, the whole process needs to be wrapped in a single synchronization mechanism. You want an atomic increment operation, you would write a method to do that.
So, taking your NSLock
example, I might move the synchronization logic into its own method, e.g.:
class Foo<T> {
private let lock = NSLock()
private var _value: T
init(value: T) {
_value = value
}
var value: T {
get { lock.synchronized { _value } }
set { lock.synchronized { _value = newValue } }
}
}
extension NSLocking {
func synchronized<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
But if you wanted to have an operation to increment the value in a thread-safe manner, you would write a method to do that, e.g.:
extension Foo where T: Numeric {
func increment(by increment: T) {
lock.synchronized {
_value += increment
}
}
}
Then, rather than this non-thread-safe attempt:
foo.value += 1
You would instead employ the following thread-safe rendition:
foo.increment(by: 1)
This pattern, of wrapping the increment process in its own method that synchronizes the whole operation, would be applicable regardless of what synchronization mechanism you use (e.g., locks, GCD serial queue, reader-writer pattern, os_unfair_lock
, etc.).
For what it is worth, the Swift 5.5 actor
pattern (outlined in SE-0306) formalizes this pattern. Consider:
actor Bar<T> {
var value: T
init(value: T) {
self.value = value
}
}
extension Bar where T: Numeric {
func increment(by increment: T) {
value += increment
}
}
Here, the increment
method is automatically an “actor-isolated” method (i.e., it will be synchronized) but the actor
will control interaction with the setter for its property, namely if you try to set value
from outside this class, you will receive an error:
Actor-isolated property 'value' can only be mutated from inside the actor