Foundation Threading Utilities

Survey Foundation's lower-level threading primitives -- NSThread, NSLock, NSRecursiveLock, NSCondition, and NSConditionLock -- understand exactly when each is needed, and recognize why modern Objective-C code mostly reaches for GCD and NSOperationQueue instead.

Before GCD existed, Foundation's threading utilities were the primary way Objective-C code managed concurrency directly, and a substantial amount of code still in production -- especially in older, long-lived codebases -- uses them. This lesson exists less to convince you to write new code with NSThread and NSLock, and more to make sure you can read, maintain, and correctly reason about existing code that does, and to give you the judgment to recognize the rare cases where one of these lower-level primitives is still genuinely the right tool, such as NSRecursiveLock's specific niche for reentrant code paths.

NSThread: The Lowest-Level Abstraction

NSThread is Foundation's direct wrapper around an operating system thread. You can create one explicitly with +detachNewThreadWithBlock: or +detachNewThreadSelector:toTarget:withObject:, or by allocating an NSThread instance and calling -start on it, and the block or selector you provide runs on a brand-new OS thread that exists until that code finishes (or the thread is otherwise torn down). Unlike GCD, where you submit work to a queue and the system decides how many actual OS threads to use underneath, NSThread gives you a thread as a directly manipulable object: you can name it, query isMainThread, set its stack size, and adjust its thread priority.

This direct control is also exactly the problem. Every NSThread you create is a real, relatively expensive OS resource, and nothing about the API helps you avoid creating too many of them, reusing them across tasks, or load-balancing work across available CPU cores -- all things GCD's thread pool handles automatically and efficiently. Code that creates a new NSThread for every incoming unit of work, under real load, can exhaust system resources or spend more time context-switching between threads than doing useful work, a failure mode GCD was specifically designed to avoid by managing a shared, appropriately-sized pool of worker threads for you.

For essentially all new development today, GCD queues or NSOperationQueue are preferred over managing NSThread directly, precisely because they abstract away exactly this bookkeeping. NSThread remains genuinely useful in a narrow set of cases: when you need a long-lived thread with its own run loop for a specific purpose (for example, a thread dedicated to processing a continuous stream of events via a custom NSRunLoop), or when interfacing with lower-level or legacy APIs that expect a literal, individually-addressable thread object rather than queue-based work.

NSLock: A Basic Mutex Object

NSLock is Foundation's simplest lock: an object with -lock and -unlock methods implementing a standard mutual-exclusion mutex. Calling -lock blocks the calling thread until the lock is available, then acquires it; calling -unlock releases it, allowing another waiting thread to acquire it next. Between -lock and -unlock, the calling thread has exclusive access to whatever code and data that lock is meant to protect -- exactly the mutual-exclusion concept the previous lesson introduced conceptually, now with a concrete Foundation object.

NSLock also offers -tryLock, a non-blocking variant that attempts to acquire the lock and returns immediately with a boolean indicating success or failure, rather than blocking the calling thread if the lock is already held. This is useful when a thread has other useful work it could do instead of blocking -- for example, checking whether a shared resource is currently free and falling back to an alternate code path if not, rather than stalling.

NSLock is not recursive: if a thread that already holds an NSLock calls -lock on that same lock again, before unlocking it the first time, the thread deadlocks against itself, waiting forever for a lock it itself is holding. This single fact is the entire reason NSRecursiveLock exists, and it is worth understanding the exact mechanism, covered next, rather than just the fact.

NSRecursiveLock: Solving Self-Deadlock

Consider a recursive method that needs to protect its own critical section with a lock -- for example, a recursive tree-traversal function that mutates a shared structure at each level of recursion. If that method acquires a plain NSLock at its start and releases it at its end, then a recursive call to itself, from within its own locked region, attempts to acquire the very same lock a second time on the very same thread. Since NSLock has no notion of 'the thread that already holds me is trying again' -- it only tracks whether the lock is held by anyone at all -- the second -lock call blocks, waiting for the lock to be released. But it will never be released, because the only thread that could release it (this same thread) is itself blocked waiting on that second -lock call. The thread is deadlocked against its own held lock.

NSRecursiveLock solves exactly this problem by tracking both which thread holds the lock and how many times that same thread has acquired it. When the holding thread calls -lock again, NSRecursiveLock recognizes that the calling thread is the same thread that already holds it, increments an internal acquisition count, and lets the call proceed immediately without blocking. Each matching -unlock call decrements that count, and the lock is only actually released -- becoming available to a different thread -- once the count returns to zero, meaning every nested -lock call has been matched by a corresponding -unlock.

The concrete use case, as the name suggests, is recursive algorithms and reentrant code paths that may re-acquire the same lock from the same thread before fully unwinding -- a recursive traversal that locks shared state at each level, or a method that might call itself indirectly through a chain of other calls while still holding its own lock. If you are not certain a piece of protected code could ever be entered again, on the same thread, before it finishes -- directly or through some indirect call chain -- NSRecursiveLock is the safer default over plain NSLock; the cost of that safety is a small amount of extra bookkeeping overhead per lock/unlock pair.

NSCondition: Locks Plus Condition Variables

NSCondition combines a mutex with a condition variable, giving you a tool for the classic pattern where a thread needs to block until some condition, tracked by shared state, becomes true -- rather than busy-waiting (repeatedly checking the condition in a tight loop, wasting CPU) or using an ad hoc sleep-and-recheck approach. You still call -lock and -unlock around the shared state exactly as with NSLock, but you additionally get -wait, -signal, and -broadcast.

Calling -wait while holding the lock atomically releases the lock and puts the calling thread to sleep, without any window where another thread could sneak in and change the condition between the release and the sleep; when another thread later calls -signal (waking one waiting thread) or -broadcast (waking all waiting threads) on the same NSCondition, the sleeping thread wakes, automatically reacquires the lock before -wait returns, and can then recheck the condition it was waiting on.

The classic producer-consumer pattern uses this directly: a consumer thread does [condition lock]; while (queueIsEmpty) { [condition wait]; } id item = [self dequeue]; [condition unlock]; while a producer thread does [condition lock]; [self enqueue:item]; [condition signal]; [condition unlock];. Note the while loop around -wait rather than a plain if: -wait can, in rare cases, wake up without the condition actually being true (a spurious wakeup), so re-checking the condition after waking, rather than assuming it must now hold, is the correct and standard pattern.

NSConditionLock: A Higher-Level Condition-Based Lock

NSConditionLock builds a more structured abstraction on top of the same underlying idea as NSCondition, associating the lock with an integer 'condition' value you define the meaning of yourself. Instead of calling -wait and manually re-checking a boolean flag in a loop, you call -lockWhenCondition: with an integer, and the calling thread blocks until another thread sets the lock's condition to exactly that value via -unlockWithCondition:.

This reframes producer-consumer-style coordination in terms of named states rather than raw wait/signal calls: for example, defining constants kBufferEmpty = 0 and kBufferFull = 1, a consumer calls [lock lockWhenCondition:kBufferFull] to wait specifically for the full state, processes the item, then calls [lock unlockWithCondition:kBufferEmpty] to both release the lock and announce the new state in one call. A producer does the mirror image, waiting for kBufferEmpty and unlocking with kBufferFull.

NSConditionLock is particularly well suited to state-machine-like coordination where there are a small number of distinct, nameable states a piece of shared work moves between, and different threads care about different specific states -- it makes that intent more readable at the call site than an NSCondition-based loop checking an arbitrary boolean, at the cost of being a slightly less flexible, more specialized tool.

@synchronized Revisited

The previous lesson covered @synchronized(obj) { ... } in the context of general thread-safety strategy; it belongs here too, as the simplest member of Foundation's threading toolkit in practical terms, since no explicit lock object needs to be allocated, stored as an ivar, or manually paired with an unlock call. The runtime maintains a recursive mutex associated with the object identity you pass in, acquires it on entry to the block, and releases it on exit -- including when an exception unwinds through the block.

As the previous lesson noted, this convenience trades away some performance relative to a dedicated NSLock or NSRecursiveLock, due to the runtime's per-object lock lookup, and its default recursiveness, while safe, can mask reentrancy that a plain NSLock would have surfaced as an explicit deadlock bug worth fixing. For quick, low-contention critical sections, particularly ones that might legitimately be reentered from the same thread, @synchronized remains a perfectly reasonable choice -- it is simply not the default reach for every case, the same way NSRecursiveLock is not either.

NSRecursiveLock Use Cases in Practice

Beyond the textbook recursive-tree-traversal example, NSRecursiveLock earns its keep in a handful of recurring real-world shapes. One is a class whose public methods call each other internally -- method A locks, does some work, and calls method B, which also needs to lock the same state; if A and B are both public entry points that must each be independently safe to call from outside, but also need to call each other internally while already holding the lock, a plain NSLock deadlocks the moment A calls B, while NSRecursiveLock handles it transparently.

Another common shape is delegate or callback-driven code where a locked method might, indirectly, trigger a callback that calls back into the same object -- for instance, a locked method that posts a notification or invokes a delegate method, and that delegate's implementation happens to call back into the original object before the original method has finished and unlocked. This kind of indirect reentrancy is easy to introduce accidentally as a codebase grows and call chains lengthen, and NSRecursiveLock is the tool that keeps such code correct without requiring every future maintainer to trace the entire call graph to prove no reentrancy can occur.

The general engineering judgment worth taking away: reach for NSRecursiveLock, or the recursive-by-default @synchronized, whenever you cannot rule out same-thread reentrancy into a locked region, either directly (an obviously recursive function) or indirectly (through callbacks, delegates, or notifications); reach for plain NSLock, which is marginally cheaper, when you are confident the protected region is never reentered by its own thread, and treat an unexpected deadlock under NSLock as a signal that some reentrant path you had not accounted for actually exists.

Why Modern Code Prefers GCD and NSOperationQueue

Having walked through NSThread, NSLock, NSRecursiveLock, NSCondition, and NSConditionLock in detail, it is worth being direct about their place in modern Objective-C: in most current codebases, GCD queues and NSOperationQueue have largely replaced manual NSThread- and NSLock-style code for new development. GCD's queues compose better -- submitting work to a queue, or expressing a dependency between NSOperation objects, reads more clearly than a hand-built thread-plus-lock arrangement and is far less prone to the specific bug classes this and the previous lesson covered (forgotten unlocks, self-deadlock on a non-recursive lock, lock-ordering deadlocks between two locks acquired in inconsistent order across call sites).

GCD and NSOperationQueue also manage the underlying thread pool automatically, sizing it to the hardware and current system load rather than requiring you to decide how many NSThread instances to create, and they integrate naturally with the higher-level coordination primitives -- dispatch groups, barriers, semaphores, and NSOperation dependencies -- that this course's earlier concurrency modules covered, none of which have a clean equivalent built directly on raw NSThread and NSLock.

None of this makes the primitives in this lesson obsolete knowledge, however. Two practical reasons remain to know them well: first, a great deal of existing, still-running Objective-C code -- in mature apps, in system frameworks, in third-party libraries with long histories -- was written before GCD existed or before its idioms were fully adopted, and maintaining that code means being able to read and reason about NSThread and NSLock-based synchronization correctly rather than treating it as a black box. Second, in the genuinely rare cases where you need precise, low-level control -- a dedicated long-lived thread with its own run loop, or a hand-tuned condition-variable-based coordination scheme that does not map cleanly onto GCD's higher-level abstractions -- these primitives, and specifically NSCondition and NSConditionLock, remain the right tool, and recognizing that rare case for what it is is itself part of good engineering judgment.

Foundation Threading Primitives

NSLock is not recursive; re-locking it on the same thread deadlocks. NSCondition's -wait must be called inside a while loop re-checking the condition, not a plain if. NSConditionLock associates an integer state with the lock itself.

// NSThread
[NSThread detachNewThreadWithBlock:^{
    // runs on a new OS thread
}];

// NSLock
NSLock *lock = [[NSLock alloc] init];
[lock lock];
// ... critical section ...
[lock unlock];
BOOL got = [lock tryLock]; // non-blocking attempt

// NSRecursiveLock
NSRecursiveLock *rlock = [[NSRecursiveLock alloc] init];
[rlock lock];
[rlock lock]; // same thread: does not deadlock
[rlock unlock];
[rlock unlock];

// NSCondition
NSCondition *cond = [[NSCondition alloc] init];
[cond lock];
while (!conditionIsTrue) { [cond wait]; }
[cond unlock];
// elsewhere:
[cond lock];
conditionIsTrue = YES;
[cond signal]; // or broadcast
[cond unlock];

// NSConditionLock
NSConditionLock *clock = [[NSConditionLock alloc] initWithCondition:0];
[clock lockWhenCondition:1];
// ... work ...
[clock unlockWithCondition:0];

Code Examples

The marked BUG line calls back into the same method on the same object, on the same thread, while _lock is still held from the outer call. Because NSLock has no concept of 'this thread already holds me,' the inner -lock call blocks waiting for the lock to be released -- but only the outer, now-blocked call could release it, so the thread waits forever on a lock it is itself holding.

[self incrementCountRecursively]; // BUG Common mistake: Using a plain NSLock in a method that can call itself, directly or indirectly, on the same thread.

With NSRecursiveLock, a re-entrant -lock call from the same thread that already holds the lock succeeds immediately instead of blocking: the lock tracks an acquisition count per holding thread, incrementing on each nested -lock and decrementing on each matching -unlock, only truly releasing once the count reaches zero. This is exactly the shape of bug NSRecursiveLock exists to prevent.

NSRecursiveLock *_lock;

-consume waits, inside a while loop rather than an if, until _items has at least one element; -wait releases the lock while sleeping (so -produce can acquire it) and reacquires it automatically before returning. The while loop guards against the possibility of a spurious wakeup, where -wait returns without -signal actually having been called for this particular condition change.

while (_items.count == 0) { [_condition wait]; } -put blocks until the lock's condition is kBufferEmpty, then unlocks announcing kBufferFull; -take mirrors this, blocking for kBufferFull and unlocking back to kBufferEmpty. The named integer states make the single-slot handoff protocol explicit at each call site, which is exactly the readability NSConditionLock adds over an NSCondition-based loop checking a raw boolean.

[_lock lockWhenCondition:kBufferEmpty];

@interface TreeNode : NSObject
@property (nonatomic, strong) NSMutableArray<TreeNode *> *children;
@end

@implementation TreeNode {
    NSLock *_lock; // WRONG choice for this use case
}

- (void)incrementCountRecursively {
    [_lock lock];
    self.visitCount++;
    for (TreeNode *child in self.children) {
        [child incrementCountRecursively]; // different object, different lock: fine
        [self incrementCountRecursively];  // BUG: same object, same lock, same thread -> deadlock
    }
    [_lock unlock];
}

@end
@implementation TreeNode {
    NSRecursiveLock *_lock; // correct choice
}

- (void)incrementCountRecursively {
    [_lock lock];
    self.visitCount++;
    for (TreeNode *child in self.children) {
        [child incrementCountRecursively];
    }
    [_lock unlock];
}

@end
@implementation BoundedBuffer {
    NSMutableArray *_items;
    NSCondition *_condition;
}

- (instancetype)init {
    if (self = [super init]) {
        _items = [NSMutableArray array];
        _condition = [[NSCondition alloc] init];
    }
    return self;
}

- (void)produce:(id)item {
    [_condition lock];
    [_items addObject:item];
    [_condition signal]; // wake one waiting consumer
    [_condition unlock];
}

- (id)consume {
    [_condition lock];
    while (_items.count == 0) {
        [_condition wait]; // atomically unlocks and sleeps; re-locks on wake
    }
    id item = _items.firstObject;
    [_items removeObjectAtIndex:0];
    [_condition unlock];
    return item;
}

@end
static const NSInteger kBufferEmpty = 0;
static const NSInteger kBufferFull = 1;

@implementation SingleSlotBuffer {
    id _item;
    NSConditionLock *_lock;
}

- (instancetype)init {
    if (self = [super init]) {
        _lock = [[NSConditionLock alloc] initWithCondition:kBufferEmpty];
    }
    return self;
}

- (void)put:(id)item {
    [_lock lockWhenCondition:kBufferEmpty];
    _item = item;
    [_lock unlockWithCondition:kBufferFull];
}

- (id)take {
    [_lock lockWhenCondition:kBufferFull];
    id item = _item;
    _item = nil;
    [_lock unlockWithCondition:kBufferEmpty];
    return item;
}

@end

NSLock vs NSRecursiveLock on re-entry

The exact mechanism by which NSRecursiveLock avoids the self-deadlock a plain NSLock suffers.

Thread T calls outer() which locks L, then calls inner()
  which also locks L, still on thread T:

  Plain NSLock:
    T: lock(L) -> acquired
    T: lock(L) again -> BLOCKS (waiting on itself) -> DEADLOCK

  NSRecursiveLock:
    T: lock(L) -> acquired, count = 1
    T: lock(L) again -> same thread, count = 2, proceeds
    T: unlock(L) -> count = 1, still held
    T: unlock(L) -> count = 0, released

Important Rules

NSThread gives direct, low-level control over an OS thread but no automatic pooling; prefer GCD/NSOperationQueue for ordinary background work.

NSLock is not recursive: re-acquiring it on the same thread before unlocking deadlocks that thread against itself.

NSRecursiveLock tracks the holding thread and an acquisition count, letting the same thread re-lock safely.

NSCondition's -wait must be called in a while loop re-checking the condition, to guard against spurious wakeups.

NSConditionLock associates an integer state with the lock, making state-based coordination explicit at the call site.

In most new Objective-C code, GCD and NSOperationQueue replace manual NSThread/NSLock code; know these primitives to read and maintain existing code, and for the rare case a lower-level primitive is genuinely the right tool.

Maintaining a pre-GCD synchronization layer

Long-lived Objective-C codebases -- some system frameworks and mature commercial apps included -- still contain data-access layers written before GCD's idioms were standard practice, built directly on NSLock or NSRecursiveLock guarding a shared in-memory cache or document model. Understanding exactly why the original author reached for NSRecursiveLock instead of NSLock at a particular call site (usually: some method in that class calls another method in the same class while already holding the lock) is often the difference between safely extending that code and introducing a subtle self-deadlock the first time a new code path reenters the same lock unexpectedly.

Using NSLock to protect a method that can call itself, directly or indirectly through a callback, on the same thread.

Use NSRecursiveLock (or @synchronized) whenever same-thread reentrancy into the locked region is possible.

NSLock has no notion of the currently-holding thread re-acquiring it; a second -lock call from that same thread blocks forever, since only that same (now-blocked) thread could ever unlock it.

Calling -wait inside an if (!conditionIsTrue) check instead of a while loop.

Always re-check the condition in a while loop around -wait, since spurious wakeups can return from -wait without the condition actually being satisfied.

An if-guarded wait proceeds as soon as -wait returns even once, but a spurious wakeup means the condition may still be false; only a while loop correctly re-verifies and re-waits if necessary.

Creating a new NSThread for every incoming unit of background work in a busy code path.

Submit the work to a GCD queue or an NSOperationQueue and let the system manage a shared, appropriately-sized thread pool.

Each NSThread is a real, relatively expensive OS resource with no automatic pooling or reuse; creating many of them under load leads to excessive context-switching and resource exhaustion that GCD's managed thread pool is specifically designed to avoid.

Reaching for NSThread and NSLock by default for all new concurrent code in 2026.

Default to GCD queues or NSOperationQueue, and reach for NSThread/NSLock-family primitives only for legacy code maintenance or genuinely low-level needs like a dedicated run-loop thread.

GCD and NSOperationQueue compose better, manage threading resources automatically, and are far less prone to the deadlock and forgotten-unlock bug classes that manual NSThread/NSLock code is exposed to; the lower-level primitives remain necessary knowledge for reading and maintaining existing code, not as a first choice for new code.

Why is NSThread generally not preferred over GCD or NSOperationQueue for new background work? A. NSThread cannot run any Objective-C code B. NSThread gives no automatic pooling or reuse, so creating many of them under load wastes resources that GCD's managed pool avoids C. NSThread objects cannot be named or prioritized D. NSThread is only available on macOS, not iOS

B. NSThread gives no automatic pooling or reuse, so creating many of them under load wastes resources that GCD's managed pool avoids — Each NSThread is an individually created, relatively expensive OS thread with no built-in pooling; GCD and NSOperationQueue manage a shared, appropriately-sized thread pool automatically, which is safer and more efficient for most background work.

What happens if a thread that already holds a plain NSLock calls -lock on that same lock again, before unlocking? A. The second call succeeds immediately, just like NSRecursiveLock B. It throws an NSLockException C. The thread deadlocks, blocking forever waiting on a lock only it could release D. The lock silently resets to the unlocked state

C. The thread deadlocks, blocking forever waiting on a lock only it could release — NSLock has no concept of the currently-holding thread re-acquiring it; the second -lock call blocks waiting for release, but only that same, now-blocked thread could ever call -unlock, so it waits forever.

How does NSRecursiveLock avoid the self-deadlock a plain NSLock would suffer in the same scenario? A. It ignores -lock calls made while already locked B. It tracks the holding thread and an acquisition count, allowing the same thread to re-lock and only truly releasing once every nested lock has a matching unlock C. It automatically spawns a new thread for the recursive call D. It converts recursive calls into asynchronous GCD blocks internally

B. It tracks the holding thread and an acquisition count, allowing the same thread to re-lock and only truly releasing once every nested lock has a matching unlock — NSRecursiveLock recognizes when the calling thread is the same thread that already holds the lock, increments an internal count instead of blocking, and only releases the lock to other threads once every -lock has been matched by an -unlock.

Why must NSCondition's -wait be called inside a while loop that re-checks the condition, rather than a single if check? A. Because -wait always returns false the first time B. Because a spurious wakeup can cause -wait to return without the condition actually being true, so it must be re-verified C. Because while loops are required syntactically by the compiler for -wait D. Because -wait releases the lock permanently after one call

B. Because a spurious wakeup can cause -wait to return without the condition actually being true, so it must be re-verified — NSCondition's -wait can, in rare cases, return without the awaited condition actually holding (a spurious wakeup); the standard and correct pattern is a while loop that re-checks the condition and calls -wait again if it is not actually satisfied.

What does NSConditionLock add over a plain NSCondition-based wait/signal loop? A. It removes the need for any locking at all B. It associates the lock with a programmer-defined integer state, letting code wait for and set specific named conditions directly via lockWhenCondition:/unlockWithCondition: C. It is strictly faster than NSCondition in all cases D. It automatically retries failed operations

B. It associates the lock with a programmer-defined integer state, letting code wait for and set specific named conditions directly via lockWhenCondition:/unlockWithCondition: — NSConditionLock builds a higher-level, state-based abstraction on the same underlying idea, letting you express 'wait for state X' and 'unlock and announce state Y' directly, which is often more readable than a raw boolean checked in a wait loop.

What is the main reason this lesson's primitives (NSThread, NSLock, NSRecursiveLock, NSCondition, NSConditionLock) are still worth learning today? A. They are faster than GCD and NSOperationQueue in every case B. Apple has deprecated GCD in favor of them C. A great deal of existing code still uses them, and in rare cases -- like a dedicated run-loop thread -- they remain the genuinely right low-level tool D. They are required for all UIKit and AppKit code

C. A great deal of existing code still uses them, and in rare cases -- like a dedicated run-loop thread -- they remain the genuinely right low-level tool — GCD and NSOperationQueue have largely replaced these primitives for new development, but maintaining existing code that uses them requires understanding them correctly, and a narrow set of genuinely low-level use cases -- like a dedicated thread with its own run loop -- still calls for them directly.

Implement a thread-safe LRU-ish single-item cache class using NSCondition: -store: blocks if an item is already present until -take is called by another thread to clear it, then stores the new item and signals; -take blocks until an item is present, then removes and returns it. Then write a second implementation of the same behavior using NSConditionLock with two named states (kSlotEmpty, kSlotFull) instead, and compare the readability of the two versions in a comment. Hint: The NSCondition version needs a while loop around each -wait checking the slot's occupancy flag; the NSConditionLock version replaces that boolean and the wait loop with lockWhenCondition:/unlockWithCondition: calls.

Try it yourself — there's no single right answer here; compare your result against this lesson's examples.