Objective-C Concurrency Memory Model
Precisely distinguish thread-safety, atomicity, race-freedom, and deadlock-freedom as four separate, non-overlapping guarantees; understand visibility and the happens-before concept as the foundation of correct concurrent code; recognize a data race as undefined behavior; and apply the practical tools — @synchronized, NSLock, dispatch_barrier_async, serial queues, immutable data, and C11 atomics — that establish real synchronization boundaries in Objective-C code.
Most developers learn Grand Central Dispatch as a set of APIs — queues, dispatch_async, dispatch_sync — long before they learn the memory model those APIs actually rest on, and that gap is where a huge number of subtle, hard-to-reproduce concurrency bugs live. Marking a property atomic feels like it should make a class thread-safe. It does not, and the gap between what atomic actually guarantees and what 'thread-safe' actually means is one of the most consequential misconceptions in all of Objective-C, precisely because code built on that misconception often works fine for months before failing under just the right timing. This lesson goes underneath the API surface to the concepts that make concurrent code correct or incorrect in the first place: atomicity, visibility, data races, the happens-before relationship, and the concrete tools — locks, barriers, serial queues, immutability, and atomics — that create real synchronization boundaries between threads.
Four Words That Are Not Synonyms
Thread-safe, atomic, race-free, and deadlock-free sound like they describe the same general idea — 'this concurrent code is fine' — and conflating them is one of the most common and most consequential mistakes in concurrent programming. Each one is a distinct, precise guarantee, none of them implies any of the others, and a piece of code can have any combination of these properties present or absent independently. The diagram below lays out the four terms this lesson distinguishes in depth; treat it as a checklist to return to, not a hierarchy where one implies the rest.
Thread-Safe vs. Atomic: The Critical Misconception
'Thread-safe' is a property of an operation or a type as a whole: it means that operation can be called correctly from multiple threads concurrently, with no possibility of corrupting internal state or producing an inconsistent result, no matter how the calls happen to interleave. 'Atomic,' in the specific sense Objective-C's atomic property attribute uses, is a much narrower guarantee about a single operation: it means that operation — specifically, the individual get or the individual set of that one property — will not tear. Tearing, here, means a reader can never observe a half-written value: for a property backed by more than a single machine word (an NSString pointer plus retain-count bookkeeping, for instance), a torn read could otherwise return a pointer that is neither the old object nor the new one but some inconsistent mixture of both, which would be catastrophic. Marking a property atomic prevents exactly that specific failure and nothing more.
The critical misconception is believing that atomic therefore makes the class thread-safe. It does not, because atomic only protects each individual property access in isolation — it says nothing whatsoever about a compound operation built from more than one access. A read-modify-write sequence — read a counter's current value, add one, write the new value back — is not made safe by the counter property being atomic, because two threads can each perform an atomic read of the same starting value, each independently compute value + 1, and each perform an atomic write of that same incremented result, losing one of the two increments entirely even though every single individual get and set was, in isolation, perfectly atomic. Atomic guarantees the property survives simultaneous access without producing a corrupted, torn value; it says nothing about whether a sequence of several atomic accesses, taken together, produces the result you actually intended.
This is precisely why nonatomic is the default recommendation for most properties in real Objective-C code once you actually need thread safety: atomic adds real synchronization overhead to every single access for a guarantee (no torn reads) that, on its own, rarely solves the actual concurrency problem a class has, while the real problem — coordinating compound operations across threads — needs one of the tools covered later in this lesson (a lock, a serial queue, a barrier) regardless of whether the underlying properties are atomic or not.
Race-Free and Deadlock-Free: The Other Two Axes
Race-free is a guarantee that no two threads ever access the same memory location concurrently with at least one of them writing, without some synchronization mechanism establishing an order between those accesses — the precise condition covered in depth two sections from now. Code can be atomic at the level of individual operations yet still not be race-free once you look at how multiple operations compose, exactly as the previous section demonstrated with a naive incrementing counter.
Deadlock-free is a separate guarantee again: it means the code's synchronization never reaches a state where two or more threads are permanently stuck, each waiting on a resource the other holds, with no possible path forward. Code can be perfectly race-free — every shared access is properly locked, with no torn or inconsistent reads ever possible — and still deadlock, if, for instance, two different locks are acquired in inconsistent order on two different threads (thread A locks lock1 then waits for lock2, while thread B locks lock2 then waits for lock1), or if a lock is acquired reentrantly in a way the lock type does not support. Race-freedom is about the correctness of the data threads see; deadlock-freedom is about whether the threads make forward progress at all — a program can score well on one axis and completely fail the other.
Putting all four together: thread-safe is the outcome you actually want for a type or an operation as a whole; atomic is a narrow, specific guarantee about one property access not tearing; race-free is the absence of unsynchronized concurrent access to shared memory; and deadlock-free is the guarantee that your synchronization mechanism itself never locks up the threads using it. A genuinely thread-safe class needs to be both race-free and deadlock-free in every one of its operations — atomicity alone gets you neither.
Visibility: Why a Write on One Thread May Not Appear on Another
Beyond the question of whether two threads corrupt each other's writes lies a subtler problem: even when no corruption occurs, one thread's write to a piece of memory is not automatically guaranteed to become visible to another thread at all, let alone promptly. Modern CPUs have multiple levels of cache, and a given core's write to a variable may sit in that core's own cache for some time before it is flushed out to memory or made visible to other cores' caches; on top of that, both the compiler and the CPU itself are allowed to reorder instructions that have no apparent dependency between them, as long as the reordering does not change the observable behavior of the single thread doing the writing. Neither of those optimizations account for another thread that might be watching the same memory location, which means, without an explicit synchronization point, a second thread can genuinely keep observing a stale value indefinitely, or observe writes to two different variables in an order different from the order the first thread actually performed them in.
This is not a hypothetical or a rare hardware quirk — it is a routine consequence of how modern multi-core CPUs and optimizing compilers are built to run single-threaded code as fast as possible, with any guarantee about cross-thread visibility being something you must explicitly establish rather than something you get automatically. A memory barrier (sometimes called a fence) is the mechanism that establishes that guarantee: it forces the CPU to flush pending writes and prevents certain reorderings across the barrier, and every synchronization primitive covered later in this lesson — a lock's acquire and release, a dispatch barrier, a queue hand-off — carries an implicit memory barrier as part of what makes it a correct synchronization tool in the first place, not just a mutual-exclusion mechanism.
Data Races: Undefined Behavior, By Definition
A data race has a precise definition, not a fuzzy one: it occurs when two or more threads access the same memory location concurrently, at least one of those accesses is a write, and there is no synchronization establishing an ordering between them. When all three conditions hold, the behavior is undefined — not merely 'might produce a surprising value,' but genuinely undefined in the same sense as any other undefined behavior in C-derived languages, meaning the compiler and CPU are under no obligation to produce any particular, predictable outcome at all. In practice a data race can manifest as a torn read, a lost update, a value that appears to jump backward in time from one thread's perspective, or — because the compiler is allowed to assume data races do not occur when applying optimizations — code that behaves correctly in an unoptimized debug build and misbehaves in a release build compiled with optimizations enabled, which is exactly the kind of bug that only appears after shipping.
It is worth being explicit that two threads merely reading the same memory location concurrently, with no writer involved at all, is not a data race — reads alone never conflict. The moment even one of the concurrent accesses is a write, though, the absence of synchronization makes the combination undefined, regardless of how 'small' or 'simple' the write looks in source code, which is precisely why the plain-int-counter example in this lesson's mistakes section below is a genuine data race even though incrementing an int looks, at the source level, like the simplest possible operation there is.
The Happens-Before Concept
Happens-before is the concept that gives 'synchronization' its actual meaning rather than leaving it as a vague notion of 'using a lock somewhere.' One operation is said to happen-before another when the language's memory model guarantees that every effect of the first operation — every memory write it performed — is guaranteed visible to whatever thread performs the second operation, once the second operation has actually happened. Ordinary sequential statements within a single thread trivially happen-before each other in program order, but establishing a happens-before relationship across two different threads requires an explicit synchronization point connecting them: releasing a lock happens-before a subsequent acquisition of that same lock by another thread; a block submitted with dispatch_async happens-before the execution of that block on whatever queue it runs on; and, on a serial queue specifically, one submitted block finishing happens-before the next submitted block beginning, which is exactly the property that makes serial queues useful as a synchronization mechanism in the next section.
The reason this concept matters practically, rather than just academically, is that it is the actual test for whether a piece of concurrent code is correct: if thread A writes some data and thread B later reads it, the code is correct only if there is a genuine happens-before relationship chaining A's write to B's read — through a shared lock, a queue hand-off, a barrier, or some other explicit synchronization primitive — and not merely because, in your own testing, B's read always seemed to happen after A's write in wall-clock time. Wall-clock ordering during testing is not a substitute for an established happens-before relationship; it is exactly the false confidence that lets a data race hide until a change in timing, hardware, or optimization level exposes it.
Synchronization Tools That Establish Real Boundaries
Objective-C and Grand Central Dispatch give you several concrete tools for creating an actual happens-before relationship between threads, each with a different shape and a different cost, and choosing between them is a real design decision rather than a matter of taste.
• @synchronized(obj) { ... } — a convenience syntax around a per-object recursive lock, keyed by the object passed in. Simple to use for occasional, low-contention protection of a small critical section, but it has real overhead (it involves exception-handling setup internally) and offers no way to try a lock non-blockingly, which makes it a poor fit for hot paths or complex locking hierarchies. • NSLock (and its relatives NSRecursiveLock and NSConditionLock) — an explicit mutex object with lock and unlock methods. More explicit and slightly lower-overhead than @synchronized for code that acquires and releases a lock in a tight loop, but it puts the responsibility for always pairing lock with unlock — including on every early-return and every thrown exception path — entirely on you. • dispatch_barrier_async on a concurrent queue — submits a block that waits for all previously submitted blocks on that queue to finish, then runs alone (blocking any blocks submitted after it until it completes), before the queue resumes normal concurrent execution. This is the standard GCD pattern for a 'readers-writer' scenario: many concurrent reads run in parallel on the queue, while any write is submitted as a barrier block, guaranteeing it never overlaps with a read or another write. • Serial queues as a race-avoidance mechanism in their own right — rather than protecting shared state with an explicit lock, a serial queue can own that state entirely, with every read and write to it submitted as a block onto that one queue. Because a serial queue only ever runs one block at a time, in submission order, this eliminates concurrent access to the protected state by construction, without an explicit lock object anywhere in the code, and the happens-before relationship between one block finishing and the next beginning is guaranteed by the queue itself.
Immutable Data: The Simplest Race-Avoidance Strategy
Every synchronization tool above exists to safely coordinate access when at least one thread might write to shared memory. The simplest possible way to avoid a data race, by a wide margin, is to remove the write side of that equation entirely: if a piece of data is genuinely immutable once constructed — an NSString rather than an NSMutableString, an NSArray rather than an NSMutableArray, a value object whose properties are all set once at initialization and never reassigned afterward — then any number of threads can read it concurrently, forever, with zero synchronization required and zero risk of a data race, because a data race requires at least one write, and there is none to be had.
This is why immutable value types are such a consistently recommended pattern in concurrent Objective-C design: passing a fully-constructed, immutable snapshot of some state between threads (rather than a mutable object that multiple threads might poke at over time) sidesteps the entire category of problem this lesson is about, rather than solving it with locks after the fact. Where a design can reasonably choose between mutating shared state in place versus constructing and handing off a new immutable value, the immutable option is very often both simpler to reason about and genuinely faster, since it needs no locking overhead at all.
Atomics: Lock-Free Synchronization for Simple Operations
For the narrow but common case of a single shared counter or flag, C11's <stdatomic.h> (the modern, portable choice, superseding the older, now-deprecated OSAtomic family of functions) provides operations like atomic_fetch_add and atomic_compare_exchange_strong that are implemented directly with special CPU instructions — a compare-and-swap or an atomic add — rather than with an operating-system-level lock. These instructions guarantee that the entire read-modify-write sequence, not just an individual read or an individual write, completes as one indivisible unit, which is precisely the guarantee a plain atomic property does not give you, as the earlier section on thread-safe versus atomic explained in depth.
Atomics are lower overhead than a full lock for simple cases because they avoid any operating-system-level blocking or context-switch cost — the CPU itself guarantees the operation's indivisibility — but they only compose correctly for the specific, narrow operations the atomic type actually exposes (an atomic add, an atomic compare-and-swap, and similar primitives). The moment a piece of shared state needs more than one such operation to update consistently together — updating two related counters in a way that must appear atomic as a pair, for instance — atomics alone are no longer sufficient, and a lock, a barrier, or a serial queue becomes the correct tool again.
Detecting Races in Practice: Thread Sanitizer
Because a data race is undefined behavior that may or may not produce a visibly wrong result on any given run, simply testing an app and not observing a crash is weak evidence that it is race-free — the bug can sit dormant through months of testing and then surface under a different device, OS version, or compiler optimization level. Xcode's Thread Sanitizer (enabled as a scheme diagnostic, much like Address Sanitizer) instruments memory accesses at runtime and can detect an actual, in-progress data race even when it happens not to produce an incorrect result on that particular run, reporting the two conflicting accesses and both call stacks. Running a concurrency-heavy module's test suite under Thread Sanitizer at least occasionally is one of the few genuinely reliable ways to catch a race before a user's specific hardware and timing exposes it in production.
Four Ways to Establish a Real Synchronization Boundary
All four establish a genuine happens-before relationship between one thread's write and another thread's subsequent access; none of them are optional decoration — each is doing real synchronization work. Atomic properties are not in this list on purpose: atomic alone does not make a compound operation like += thread-safe, which is the central misconception this lesson addresses. Prefer the simplest tool that fits: a serial queue owning its own state is often easier to reason about correctly than scattering explicit lock/unlock pairs across many methods.
// 1. @synchronized -- a per-object recursive lock
@synchronized(self) {
_cachedTotal += amount;
}
// 2. NSLock -- an explicit mutex you must pair lock/unlock around yourself
[self.lock lock];
_cachedTotal += amount;
[self.lock unlock];
// 3. dispatch_barrier_async on a concurrent queue -- exclusive write among concurrent reads
dispatch_barrier_async(self.concurrentQueue, ^{
self->_cachedTotal += amount;
});
// 4. A serial queue owning the state entirely -- no explicit lock object at all
dispatch_async(self.serialQueue, ^{
self->_cachedTotal += amount;
});Code Examples
Marking value atomic guarantees each individual get and each individual set will not tear — no thread ever observes a half-written NSInteger. It says nothing about the compound self.value = self.value + 1 statement, which is really a read followed by a separate write; two threads can interleave between those two steps and lose an update, exactly the misconception this lesson's core section addresses.
Common mistake: atomic prevents torn reads/writes of a single property access; it does not make a read-modify-write sequence built from two accesses thread-safe.
Switching to nonatomic removes the now-pointless per-access overhead, and wrapping the entire read-modify-write sequence in @synchronized(self) makes the whole compound operation indivisible with respect to other threads calling increment on the same instance, which is what atomic alone could never provide.
@synchronized (self) { self.value = self.value + 1; } Reads are submitted with dispatch_sync directly onto the concurrent queue, so many of them can execute in parallel with no write in progress. Writes are submitted with dispatch_barrier_async, which waits for any in-flight reads to finish, then runs alone before the queue resumes concurrent execution — guaranteeing no read ever observes a dictionary mid-mutation. This is the standard GCD readers-writer pattern: cheap for the common case of many concurrent reads, while still making every write fully exclusive.
_pendingCount is never accessed directly from outside serialQueue's blocks, so it never has more than one thread touching it at a time by construction, with no explicit lock object anywhere in the class. The queue itself is the synchronization mechanism: one block finishing happens-before the next block beginning, which is exactly the happens-before relationship this lesson's earlier section defined.
dispatch_async(self.serialQueue, ^{ self._pendingCount += 1; });
atomic_fetch_add performs the entire read-modify-write as one indivisible CPU-level operation, unlike the atomic property misconception example above where the read and the write were two separate steps. This gives thread-safety for exactly this one operation without the overhead of an operating-system lock, but it only covers this specific atomic-int; it would not, on its own, make a more complex multi-field update safe.
atomic_fetch_add(&_hits, 1);
Because every property is set once, in the initializer, using copy to guard against the caller later mutating the source objects, and never reassigned afterward, a Snapshot instance has no write side once construction finishes. A data race requires at least one write among the concurrent accesses; with none possible, no synchronization is needed for any number of concurrent readers. This is the simplest of every tool in this lesson, and often the best one: instead of protecting a mutable object with locks, hand different threads their own immutable snapshot instead.
@interface Counter : NSObject
@property (atomic, assign) NSInteger value;
@end
@implementation Counter
- (void)increment {
// Looks safe because 'value' is atomic -- it is NOT.
self.value = self.value + 1; // read, then write: two separate atomic operations
}
@end
// Two threads calling [counter increment] concurrently can both read the
// same starting value before either writes back the incremented result,
// silently losing one of the two increments.@interface Counter : NSObject
@property (nonatomic, assign) NSInteger value;
@end
@implementation Counter
- (void)increment {
@synchronized (self) {
self.value = self.value + 1; // read and write now happen as one atomic step
}
}
@end@interface Cache : NSObject
@property (nonatomic, strong) dispatch_queue_t queue; // concurrent queue
@property (nonatomic, strong) NSMutableDictionary *storage;
@end
@implementation Cache
- (instancetype)init {
if (self = [super init]) {
_queue = dispatch_queue_create("com.example.cache", DISPATCH_QUEUE_CONCURRENT);
_storage = [NSMutableDictionary dictionary];
}
return self;
}
- (id)objectForKey:(NSString *)key {
__block id result;
dispatch_sync(self.queue, ^{
result = self.storage[key]; // many reads can run concurrently
});
return result;
}
- (void)setObject:(id)object forKey:(NSString *)key {
dispatch_barrier_async(self.queue, ^{
self.storage[key] = object; // runs alone, no overlap with reads or other writes
});
}
@end@interface RequestCounter : NSObject
@property (nonatomic, strong) dispatch_queue_t serialQueue;
@property (nonatomic, assign) NSInteger _pendingCount; // only ever touched on serialQueue
@end
@implementation RequestCounter
- (instancetype)init {
if (self = [super init]) {
_serialQueue = dispatch_queue_create("com.example.requestCounter", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)requestStarted {
dispatch_async(self.serialQueue, ^{
self._pendingCount += 1; // safe: serial queue guarantees one block at a time
});
}
- (void)requestFinishedWithCompletion:(void (^)(NSInteger remaining))completion {
dispatch_async(self.serialQueue, ^{
self._pendingCount -= 1;
NSInteger remaining = self._pendingCount;
dispatch_async(dispatch_get_main_queue(), ^{
completion(remaining);
});
});
}
@end#include <stdatomic.h>
@interface HitCounter : NSObject {
atomic_int _hits;
}
@end
@implementation HitCounter
- (instancetype)init {
if (self = [super init]) {
atomic_init(&_hits, 0);
}
return self;
}
- (void)recordHit {
atomic_fetch_add(&_hits, 1); // one indivisible hardware-level operation
}
- (int)currentHits {
return atomic_load(&_hits);
}
@end@interface Snapshot : NSObject
@property (nonatomic, readonly, copy) NSString *displayName;
@property (nonatomic, readonly, copy) NSArray<NSNumber *> *scores;
@end
@implementation Snapshot
- (instancetype)initWithDisplayName:(NSString *)name scores:(NSArray<NSNumber *> *)scores {
if (self = [super init]) {
_displayName = [name copy];
_scores = [scores copy];
}
return self; // fully constructed and never mutated again
}
@end
// Any number of threads may read snapshot.displayName or snapshot.scores
// concurrently, forever, with zero locking -- there is no writer to race with.Four Distinct Guarantees, Not One
None of these four implies the others. A class can be atomic-everywhere and still not be thread-safe; it can be perfectly race-free and still deadlock; genuine thread-safety requires satisfying all of the relevant guarantees for every operation the class exposes, not just one of them.
Thread-safe
vs
Atomic
vs
Race-free
vs
Deadlock-freeWhy 'It Worked in Testing' Is Not Evidence of Thread Safety
Data races are undefined behavior specifically because a compiler is permitted to assume they do not occur when optimizing code, and a CPU's cache and reordering behavior varies by core count, architecture, and even momentary system load. This combination is exactly why a race condition can survive weeks of QA on a developer's machine and then appear in production only on a specific device, only under specific load, or only after an unrelated compiler upgrade changes how aggressively a loop gets optimized — the underlying bug was present the entire time, but nothing about ordinary testing is guaranteed to expose it, since undefined behavior is under no obligation to fail visibly, or consistently, or ever, on any particular run. This is the practical reason tools like Thread Sanitizer, and disciplined use of the actual synchronization primitives this lesson covers rather than an intuition about atomic properties, matter far more for shipped reliability than 'it hasn't crashed yet' ever can.
int sharedCounter = 0; // then, from two different background queues, both running: for (int i = 0; i < 100000; i++) { sharedCounter++; } with no lock, no queue, no atomic type at all.
Use one of: a serial queue owning sharedCounter (dispatch_async(serialQueue, ^{ sharedCounter++; })), an NSLock around the increment, @synchronized around the increment, or an atomic_int with atomic_fetch_add, so the read-modify-write completes as one indivisible step.
A plain int has no synchronization built in at all. Two threads can both read the same current value of sharedCounter before either writes its incremented result back, silently losing increments — a textbook data race, and, because sharedCounter is accessed concurrently with at least one write and no synchronization, technically undefined behavior, not merely 'sometimes produces a slightly low count.'
What does the atomic property attribute actually guarantee? A. That the entire class is thread-safe for any sequence of operations B. That an individual get or set of that one property will not tear, returning a half-written value C. That no two threads can ever call any method on the object at the same time D. That the property's value can never change once first set
B. That an individual get or set of that one property will not tear, returning a half-written value — atomic guarantees only that a single get or a single set of that property completes as one indivisible operation, so a reader never observes a torn, partially-written value; it says nothing about compound operations built from more than one access.
Why doesn't marking a property atomic make a read-modify-write sequence like self.value = self.value + 1 thread-safe? A. Because atomic properties cannot be read and written in the same statement at all B. Because the read and the write are two separate atomic operations, and another thread's own read/write can interleave between them, losing an update C. Because atomic only works for object types, never for NSInteger D. Because atomic is deprecated and has no effect in modern Objective-C
B. Because the read and the write are two separate atomic operations, and another thread's own read/write can interleave between them, losing an update — self.value = self.value + 1 is a get followed by a separate set; each one individually is atomic and torn-value-free, but nothing prevents another thread from performing its own get in between this thread's get and set, so the compound operation as a whole is not atomic.
What precisely defines a data race? A. Any time two threads run at the same time, regardless of what memory they touch B. Two or more threads accessing the same memory location concurrently, with at least one access being a write, and no synchronization ordering them C. Any use of NSMutableArray from more than one thread, even sequentially D. A deadlock that occurs while two threads are both trying to acquire the same lock
B. Two or more threads accessing the same memory location concurrently, with at least one access being a write, and no synchronization ordering them — A data race requires the same memory location, concurrent access from two or more threads, at least one write among those accesses, and the absence of synchronization establishing an order between them; two threads that only read the same memory concurrently do not race.
What does it mean for one operation to 'happen-before' another, in the sense this lesson uses the term? A. It means the two operations happened to execute in that order during one particular test run B. It means the memory model guarantees every effect of the first operation is visible once the second operation, connected to it by real synchronization, has occurred C. It means the first operation is scheduled with a higher QoS class than the second D. It is a synonym for 'runs on the same thread as'
B. It means the memory model guarantees every effect of the first operation is visible once the second operation, connected to it by real synchronization, has occurred — Happens-before is a guarantee established by an actual synchronization point (a lock release/acquire pair, a queue hand-off, a barrier) — not merely the order two things were observed to run in during testing, which is not a reliable substitute for that guarantee.
Why is immutable data described as the simplest race-avoidance strategy? A. Immutable objects are always stored on the stack, which is inherently thread-safe B. A data race requires at least one write among the concurrent accesses; if the data is never written after construction, there is no write to race with, and no synchronization is needed C. Immutable objects cannot be accessed from more than one thread at all D. The compiler automatically adds locks around every immutable object's properties
B. A data race requires at least one write among the concurrent accesses; if the data is never written after construction, there is no write to race with, and no synchronization is needed — Since a data race by definition requires at least one write, data that is fully constructed once and never mutated afterward has no write side for any number of concurrent readers to race with, eliminating the need for locks, barriers, or queues for that data entirely.
How does a lock-free atomic operation like atomic_fetch_add differ from a plain atomic property's get/set? A. They are functionally identical; atomic_fetch_add is just a C-style alias for an atomic property B. atomic_fetch_add performs the entire read-modify-write as one indivisible hardware-level operation, whereas an atomic property's compound self.value = self.value + 1 is still two separate atomic steps that can interleave C. atomic_fetch_add requires an explicit NSLock internally, making it slower than an atomic property D. atomic_fetch_add only works correctly on the main thread
B. atomic_fetch_add performs the entire read-modify-write as one indivisible hardware-level operation, whereas an atomic property's compound self.value = self.value + 1 is still two separate atomic steps that can interleave — atomic_fetch_add is implemented with a single CPU-level instruction that reads, modifies, and writes as one indivisible step, closing exactly the gap that a plain atomic property's separate get-then-set leaves open for a lost update.
A class has a nonatomic NSMutableArray property called items, and two different background queues both call [self.items addObject:newItem] on the same instance concurrently, with no other synchronization anywhere in the class. Identify every one of the four properties from this lesson's opening comparison (thread-safe, atomic, race-free, deadlock-free) that this code does or does not have, and propose a fix using one of the tools covered in this lesson.
The class is not thread-safe (concurrent addObject: calls on the same NSMutableArray instance can corrupt the array's internal storage), it is not meaningfully 'atomic' in any useful sense since nonatomic was used and even atomic would not help a mutable collection's internal method, it is not race-free (two threads write to the same object concurrently with no synchronization), and while it is not itself deadlock-prone as written, that is only because there is no locking at all yet, not because the design is safe. The fix is to route every mutation of items through a single serial queue owned by the class (dispatch_async(self.serialQueue, ^{ [self.items addObject:newItem]; });) or to protect it with @synchronized(self) { [self.items addObject:newItem]; } around every access, both of which establish the happens-before relationship needed to make the mutations safe.
NSMutableArray's addObject: is not documented as safe for concurrent mutation from multiple threads at all — this is a stronger problem than the atomic-vs-compound-operation gap, since even a single call to addObject: is not safe to run concurrently with another call to it.