Concurrency Deep Dive

Fair vs Unfair Locks: NSLock and os_unfair_lock

January 19, 2026
5 min read
Featured image for blog post: Fair vs Unfair Locks: NSLock and os_unfair_lock

We've covered serial queues and actors for thread safety. But what about locks? iOS offers several, and they're not all the same—some guarantee fairness, others trade it for speed.

Follow along with the code: iOS-Practice on GitHub

The Lock Landscape

Beyond queues and actors, iOS provides several lock primitives:

LockFairnessAPIUse Case
NSLockFair (FIFO)FoundationGeneral purpose, Obj-C interop
os_unfair_lockUnfairCMaximum performance
OSAllocatedUnfairLockUnfairSwiftModern Swift (iOS 16+)

Fair vs Unfair: What's the Difference?

Fair locks (NSLock) grant access in FIFO order. If thread A requests the lock before thread B, A gets it first. Predictable, no starvation.

Unfair locks (os_unfair_lock) let any waiting thread grab the lock. A thread that just released might immediately reacquire it, starving others.

NSLock: The Fair Option

final class FairContainer: @unchecked Sendable {
    private var values: [Int] = []
    private let lock = NSLock()

    func append(_ value: Int) {
        lock.lock()
        defer { lock.unlock() }
        values.append(value)
    }
}

NSLock guarantees ordering. Under heavy contention, every thread eventually gets its turn. Tail latency is predictable.

os_unfair_lock: The Fast Option

final class UnfairContainer: @unchecked Sendable {
    private var values: [Int] = []
    private let lockPtr: UnsafeMutablePointer<os_unfair_lock>

    init() {
        lockPtr = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
        lockPtr.initialize(to: os_unfair_lock())
    }

    deinit {
        lockPtr.deinitialize(count: 1)
        lockPtr.deallocate()
    }

    func append(_ value: Int) {
        os_unfair_lock_lock(lockPtr)
        defer { os_unfair_lock_unlock(lockPtr) }
        values.append(value)
    }
}

Notice the complexity: heap allocation, manual memory management, unsafe pointers. That's the price of low-level performance.

os_unfair_lock Pitfalls

Before using os_unfair_lock, understand these gotchas:

  1. Must allocate on heap - It's a value type, but can't be copied. Use a pointer.
  2. Cannot hold across await points - Locks are thread-bound; async/await can resume on different threads.
  3. No FIFO guarantee - Threads can starve under heavy contention.
  4. Tail latency spikes - Some requests may wait much longer than average.
  5. Manual memory management - You're responsible for allocation and deallocation.

OSAllocatedUnfairLock: The Modern Wrapper

iOS 16+ provides a safer API:

@available(iOS 16.0, *)
final class ModernContainer: @unchecked Sendable {
    private let lock = OSAllocatedUnfairLock(initialState: [Int]())

    func append(_ value: Int) {
        lock.withLock { $0.append(value) }
    }

    func getAll() -> [Int] {
        lock.withLock { $0 }
    }
}

Same performance as os_unfair_lock, but:

  • No manual memory management
  • Scoped locking with withLock
  • State is encapsulated with the lock

When Fairness Matters

Choose NSLock when:

  • You care about worst-case latency, not just average
  • User-facing operations that shouldn't block unpredictably
  • Systems where starvation could cause visible issues

Choose os_unfair_lock / OSAllocatedUnfairLock when:

  • You've measured NSLock as a bottleneck
  • Operations are short and contention is low
  • Average throughput matters more than tail latency

Interview Tip

"Fair locks guarantee ordering at the cost of some throughput. Unfair locks are faster on average but can starve threads under contention. I use NSLock by default—predictable tail latency matters for user experience. I only switch to unfair locks after measuring a real bottleneck."

Originally published on deadpixel.llc

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch