Thread-safe data structure using modify accessors

I wanna preface this by saying that I could be way off course here, and it might be the case that what I'm trying to accomplish here is in fact a terrible idea β€” please let me know if this is the case.


I'm trying to implement a ThreadSafe property wrapper that prevents data races when a value is accessed from multiple threads simultaneously.
As is already known, it is not possible to implement this using the get /set accessors, since a mutating access to a value would get compiled into a two separate get and set accesses.
My understanding is that the _modify accessor would enable the correct behaviour here: by yielding a reference to the underlying wrapped value, we can turn a mutating access into a single _modify, rather than a pair of a get and a set.

This is the code I have so far, and it seems to be working, but I'm not sure if this is because it's actually how this should be implemented, or whether it's merely working as i'd expect by coincidence:

@propertyWrapper
final class ThreadSafe<Value> {
    private let lock = OSAllocatedUnfairLock()
    private var value: Value
    
    init(_ value: Value) {
        self.value = value
    }
    
    convenience init(wrappedValue value: Value) {
        self.init(value)
    }
    
    var wrappedValue: Value {
        _read {
            lock.lock()
            defer { lock.unlock() }
            yield value
        }
        _modify {
            lock.lock()
            defer { lock.unlock() }
            yield &value
        }
    }
}

(The alternative of course would be to use eg a DispatchQueue to synchronise accesses to a protected value, but that wouldn't work with the semantics of a property wrapper...)

(Also: I'm aware that modify accessors are currently still officially unsupported; i'm to an extent just curious whether this would be the overall correct approach, or whether this is the completely wrong way for trying to achieve this.)

You can achieve similar result using officially supported features

This doesn't answer your question, but you should check this out: SE-0433 (synchronous mutual exclusion lock).

Is _read + _modify better than get + _modify?


To stress test it I'd introduce a delay inside the locked fragment, it could be usleep, or a manual loop, for a fixed or randomly selected delay.


Note that this approach protects the individual property and individual operation on that property only. Sometimes this is enough but often times it is not:

Example1:

@ThreadSafe var number = 100
let n = number
number = n + 1

Example2:

@ThreadSafe var debit = 100
@ThreadSafe var credit = 200

func deposit(_ amount: Int) {
    debit -= amount
    credit += amount
}

Avoids a copy, if the caller doesn’t need to copy, at the cost of holding the lock while the value is in use.

1 Like

When does the caller not need to copy?

If you immediately access a member:

print(lockedVal.message) // does not copy lockedVal (a.k.a. _lockedVal.value)