Struct property wrapper with NSLock crashed while concurrently access

The following property wrapper will crash:

@propertyWrapper
struct Atomic<T> {
    private var value: T
    private let locker = NSLock()
    
    init(wrappedValue: T) {
        self.value = wrappedValue
    }
    
    var wrappedValue: T {
        _read {
            locker.lock()
            defer { locker.unlock() }
            yield value
        }
        _modify {
            locker.lock()
            defer { locker.unlock() }
            yield &value
        }
    }
}


final class Main: @unchecked Sendable {

    @Atomic
    var arr = [Int]()

    func run() {
        DispatchQueue.global().async {
            DispatchQueue.concurrentPerform(iterations: 100_000_000) { _ in
                self.arr.append(0)
            }
            print(self.arr.count)
        }
    }
}

let m = Main()
m.run()

But if i use class instead, it won't crash, why?

@propertyWrapper
final class Atomic<T> {
    private var value: T
    private let locker = NSLock()
    
    init(wrappedValue: T) {
        self.value = wrappedValue
    }
    
    var wrappedValue: T {
        _read {
            locker.lock()
            defer { locker.unlock() }
            yield value
        }
        _modify {
            locker.lock()
            defer { locker.unlock() }
            yield &value
        }
    }
}

Just as a couple quick notes that shouldn't be related to the crash:

  • This doesn't compile for me unless I add import Foundation
  • That kind of property wrapper is not generally sufficient to ensure thread-safety

Anyway, when I run this it doesn't appear to crash... but also doesn't print anything, which is equally odd.

can you try again?
Add the code below to the last line.

while true {}

Have you considered using Mutex or OSAllocatedUnfairLock, which jump through the necessary hoops to do this safely for you, and are also faster and use less memory?

i intentionally use NSLock and not use Mutex. as i just want to know why using struct will crash but class won't

Accessing wrappedValue on the struct via the _modify accessor requires exclusive access to the entire struct. Since that exclusivity is asserted over the whole struct, it occurs before you have a chance to grab the lock,so you have racing exclusive accesses to the arr variable and the behavior is undefined. In the class case, since accessing wrappedValue doesn't need to modify the object reference itself, exclusive access isn't needed until you access the value property of the object, which happens inside of the lock.

14 Likes

Hey Carl, the issue comes from struct value semantics. When Atomic is a struct, each time wrappedValue is accessed, a copy is made, and NSLock doesn’t protect these copies. That’s why concurrent writes lead to crashes. When using a class, the reference remains the same, and NSLock properly synchronizes access to value. Also, when debugging tricky concurrency issues, I take breaks and read Surah Yaseen (https://suraheyaseen.com/)—it helps me reset and focus. Hope this helps!