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?
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.
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!
I have been unable to reproduce your crash with your example using the struct rendition of your @Atomic property wrapper on macOS with Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1; arm64-apple-macosx15.0).
Admittedly, since you are dispatching asynchronously to a global queue, it begs the question of how you are preventing the app from terminating before the asynchronous work is done. It must not be a command line app as suggested by code in the question. Or perhaps you have something else (like your own run loop or doing this within a UIKit/AppKit/SwiftUI app) letting this run to completion before the app terminates.
Can you provide more details about your project and/or environment for this example? Can you help us reproduce the crash?
As an aside, it does seem antithetical to use a value type (a struct), with inherent copy semantics, for a property wrapper designed to manage concurrent access. If you were to do this, I might declare struct rendition of Atomic as ~Copyable (to bring to your attention any unintentional copying). But a reference type (a class) seems less constraining and more idiomatic for concurrent access. But it is hard to comment on your crash without being able to reproduce it.
The particular example you provide (where you mutate from within a closure) would seem to avoid initiating copies, but we can easily construct examples where copies would be made with unintended behaviors.
I was able to reproduce the crash with the struct.
Object 0x600000d60120 of class _ContiguousArrayStorage deallocated with non-zero retain count 2. This object's deinit, or something called from it, may have created a strong reference to self which outlived deinit, resulting in a dangling reference.
However, this confuses me—I would have expected an exclusive access to memory violation since we are accessing the locker property inside _modify.
Edit: After thinking it through, I realize that nothing happening inside _modify itself is inherently incorrect. The issue is that the lock isn’t preventing overlapping _modify calls, which is not allowed for structs. I suspect the crash above is just a side effect of the racing access.
the exact failure mode appears (unsurprisingly) nondeterministic – sometimes it crashes with SIGABRT, sometimes it aborts on retain count invariants being broken, etc. building with TSAN on will point to the racing accesses: