0

Let's say, there is a variable that I want to make thread safe. One of the most common ways to do this:

var value: A {
    get { return queue.sync { self._value } }
    set { queue.sync { self._value = newValue } }
}

However, this property is not completely thread safe if we change the value as in the example below:

Class.value += 1

So my question is: Using NSLock on the same principle is also not completely thread safe?

var value: A {
    get { 
       lock.lock()
       defer { lock.unlock() }
       return self._value
    }
    set { 
       lock.lock()
       defer { lock.unlock() }
       self._value = newValue
    }
}
V.V.V
  • 235
  • 1
  • 13
  • I would recommend POSIX based solution. [Here is good example](https://www.vadimbulavin.com/atomic-properties/) – Asperi Apr 30 '20 at 16:37
  • 3
    @Asperi - I’d be wary of that article: First, the protocol `Lock` is already exists, [`NSLocking`](https://developer.apple.com/documentation/foundation/nslocking). Second, calling unfair lock a `SpinLock` is dubious. Third, the mutex/os_unfair_lock implementations are incorrect. (See C lock discussion in WWDC 2016 [Concurrent Programming With GCD in Swift 3](https://developer.apple.com/videos/play/wwdc2016/720/?time=1085)). See https://github.com/apple/swift/blob/88b093e9d77d6201935a2c2fb13f27d961836777/stdlib/public/Darwin/Foundation/Publishers%2BLocking.swift#L18 for correct implementation. – Rob Apr 30 '20 at 20:33

2 Answers2

3

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

Rob
  • 415,655
  • 72
  • 787
  • 1,044
1

That's interesting, I'm learning about this for the first time.

The issue in the first bit of code, is that:

object.value += 1

has the same semantics as

object.value = object.value + 1

which we can further expand to:

let originalValue = queue.sync { object._value }
let newValue = origiinalValue + 1
queue.sync { self._value = newValue }

Expanding it so makes it clear that the synchronization of the getter and setter work fine, but they're not synchronized as a whole. A context switch in the middle of the code above could cause _value to be mutated by another thread, without newValue reflecting the change.

Using a lock would have the exact same problem. It would expand to:

lock.lock()
let originalValue = object._value
lock.unlock()

let newValue = originalValue + 1

lock.lock()
object._value = newValue
lock.unlock()

You can see this for yourself by instrumenting your code with some logging statements, which show that the mutation isn't fully covered by the lock:

class C {
    var lock = NSLock()

    var _value: Int
    var value: Int {
        get {
            print("value.get start")
            print("lock.lock()")
            lock.lock()
            defer {
                print("lock.unlock()")
                lock.unlock()
                print("value.get end")
            }
            print("getting self._value")
            return self._value
        }
        set { 
            print("\n\n\nvalue.set start")
            lock.lock()
            print("lock.lock()")
            defer {
                print("lock.unlock()")
                lock.unlock()
                print("value.set end")
            }
            print("setting self._value")
            self._value = newValue
        }
    }

    init(_ value: Int) { self._value = value }
}

let object = C(0)
object.value += 1
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Thanks for your answer! Yes, you are right that the problem the problem is due to the operation that is between atomic read and atomic write. It looks like this is a common mistake: synchronize in getters / setters instead of using some _modify_ methods... – V.V.V Apr 30 '20 at 16:40
  • @GringoRusso This reminds me of a similar problem. When a collection like an `Array` or `Dictionary` stores a type that implements copy-on-write, in-place mutations could cause extraneous copies. Operations like `array[0].mutate()` used to cause a `get` followed by a `set`, much like `+=` here. During mutation, 2 references to the element exist: 1 reference by the array, and 1 reference by the temporary variable that's about to mutate it. When a mutation is done on an object with a reference count >1, a copy is triggered. – Alexander Apr 30 '20 at 16:45
  • 1
    This was solved in the standard library using a `_modify`, a hidden alternative to `set`/`get`. It uses a co-routine to do a temporary mutation of an element in-place, without increment its reference count. Once it's a publicly available feature, I think it could be used here to wrap a lock around an entire mutation like this. Ben Cohen talks about it in his talk "Fast Safe Mutable State", here: https://youtu.be/BXJIIQ-B4-E?t=883 – Alexander Apr 30 '20 at 16:49