What does atomic actually guarantee for an Objective-C property, and how does Swift see nonatomic and atomic?

4.9.3 · L3 · Interview

Predict first

Eight concurrent iterations each add 1 to an atomic property 200 000 times. Is the final value exactly 1 600 000?

// Objective-C: @property (atomic) NSInteger an;
nonisolated(unsafe) let c = Counter()
DispatchQueue.concurrentPerform(iterations: 8) { _ in
    for _ in 0..<200_000 { c.an += 1 }
}
print(c.an < 1_600_000)
  • true: updates were lost
  • false: atomic makes += safe

Mental model. An atomic property is a mailbox that is never opened by two people at once. It will not stop two people from reading the number on a card, adding one, and each putting back their own card.

Objective-C properties are atomic by default. For an object property that means the synthesized getter and setter take a lock (or an equivalent) so a reader never sees a half-written pointer or a value that is mid-release. nonatomic skips that and is faster, which is why almost all UIKit-era code writes it. Swift imports both the same way: there is no atomic keyword in Swift, and an imported atomic property is an ordinary property whose accessors happen to be atomic. A Swift property you expose with @objc is generated nonatomic, and its exposure to Objective-C does not make it thread-safe.

Atomic protects one access, not a sequence. counter.n += 1 is a read followed by a write, so two threads can both read 10 and both write 11, even though each access was atomic. For compound operations you need a lock, a serial queue or an actor. That is why atomic properties are rarely worth their cost, and why it is misleading to call them “thread-safe”.

Each read and each write was atomic, but the increment is two steps, so updates overlap and are lost. The total comes out well below the sum (three trial runs here gave roughly 0.5 to 1.3 million).

Common traps

  • Treating atomic as thread safety. It guards the access only, so counters and check-then-set logic still race.
  • Assuming a Swift let or var exposed to Objective-C became atomic. It is not, and Swift 6 will still flag cross-thread use of a non-Sendable type.

They might follow up with

  • Why is atomic cheaper for an NSInteger than for an object? A word-sized value cannot be torn, though the compiler still adds the barrier.
  • What do you use for a shared counter in modern code? An actor, Mutex or an atomic type from the Synchronization module.

Related questions

  • 2.7.5 Is Array thread-safe, and how do you make shared state safe?
  • 10.10.2 How do you choose between an actor, a Mutex, an unfair lock, an atomic and a serial queue to protect shared…
  • 1.6.5 Are property wrappers thread-safe? Is lazy?

← How do Objective-C initializers and factory methods import, and what…   What are NSNotFound and the other sentinel values, and how do they… →