Mastering iOS Locks: From NSLock to os_unfair_lock and Performance Tips
This article explains the relationship between locks and multithreading on iOS, introduces various lock types such as NSRecursiveLock, NSConditionLock, OSSpinLock, os_unfair_lock, pthread_mutex, and @synchronized, demonstrates resource contention with code examples, and compares their performance and usage scenarios.
Introduction
Locks are tightly coupled with multithreading; when thread A accesses code protected by a lock, thread B must wait until A releases the lock before it can proceed.
Lock Types
Here are the main lock types used in iOS development:
NSRecursiveLock – a recursive lock that allows the same thread to acquire the lock multiple times without deadlocking.
NSConditionLock – a condition lock where the user defines a condition that must be satisfied before a thread can acquire the lock.
OSSpinLock – a spin lock that stays in user space, reducing context switches and offering the highest performance, but can waste CPU cycles under contention.
os_unfair_lock – replaces OSSpinLock to avoid priority‑inversion problems; it puts waiting threads to sleep instead of spinning.
pthread_mutex_t – a low‑level C mutex with high performance; can be used directly via the POSIX API.
@synchronized – a simple syntax‑sugar lock that internally uses objc_sync_enter and objc_sync_exit.
dispatch_semaphore_t – a GCD semaphore that can limit concurrent access to a resource.
DISPATCH_QUEUE_SERIAL – a serial GCD queue that serializes execution, effectively acting as a lock.
Resource Contention
When multiple threads read and write the same resource simultaneously, race conditions occur. The article shows a demo where three threads repeatedly call saleTicket while sleeping to simulate work, leading to outdated values overwriting newer ones.
self.count = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
- (void)saleTicket {
NSInteger oldCount = self.count;
sleep(0.1);
oldCount--;
self.count = oldCount;
NSLog(@"count: %ld, thread: %@", self.count, [NSThread currentThread]);
}The log shows that each thread reads the old value, sleeps, then writes back, causing later threads to overwrite earlier results – a classic race condition.
Spin Lock vs. Mutex
A spin lock ( OSSpinLock) continuously polls the CPU while waiting, which is fast when the wait time is short but wastes CPU cycles under heavy contention. A mutex ( pthread_mutex_lock) puts the thread to sleep, reducing CPU usage but incurring a context‑switch overhead.
Choosing the Right Lock
For short‑duration critical sections on multi‑core CPUs, a spin lock is suitable. For longer operations, a mutex or os_unfair_lock is preferable to avoid excessive CPU consumption.
Priority Inversion
When a low‑priority thread holds a lock needed by a high‑priority thread, the high‑priority thread can be blocked, leading to priority inversion. The article includes a classic diagram illustrating this problem with threads A (low), B (medium), and C (high).
Specific Lock Implementations
OSSpinLock
Deprecated due to unsafe behavior; Apple recommends os_unfair_lock on iOS 10 and later.
os_unfair_lock
A modern unfair lock that puts waiting threads to sleep, avoiding CPU waste. It must be unlocked by the same thread that locked it.
extern void os_unfair_lock_lock(os_unfair_lock_t lock);
extern bool os_unfair_lock_trylock(os_unfair_lock_t lock);
extern void os_unfair_lock_unlock(os_unfair_lock_t lock);pthread_mutex_t
Initialized with PTHREAD_MUTEX_DEFAULT for a normal mutex or PTHREAD_MUTEX_RECURSIVE for a recursive mutex. Remember to destroy it in dealloc.
- (void)dealloc {
pthread_mutex_destroy(&_lock);
}
- (void)viewDidLoad {
[super viewDidLoad];
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
pthread_mutex_init(&_lock, &attr);
pthread_mutexattr_destroy(&attr);
}
- (void)getUserInfo {
pthread_mutex_lock(&_lock);
// business logic
pthread_mutex_unlock(&_lock);
}NSLock
A high‑level wrapper around pthread_mutex. Re‑locking the same NSLock on the same thread causes a deadlock.
NSLock *lock = [[NSLock alloc] init];
[lock lock]; // work
[lock unlock];NSRecursiveLock
Allows the same thread to lock multiple times; internally uses PTHREAD_MUTEX_RECURSIVE.
self.lock = [[NSRecursiveLock alloc] init];
for (NSInteger i = 0; i < 100; i++) {
[self.lock lock];
NSLog(@"locked");
}
for (NSInteger i = 0; i < 100; i++) {
[self.lock unlock];
NSLog(@"unlocked");
}NSCondition
Combines a mutex with a condition variable, allowing threads to wait for a specific condition before proceeding.
- (void)lockMethod {
[self.condition lock];
NSLog(@"lockMethod lock");
[self.condition wait];
NSLog(@"lockMethod wait completion");
[self.condition unlock];
}
- (void)unlockMethod {
[self.condition lock];
NSLog(@"unlockMethod lock");
[self.condition broadcast];
sleep(1);
NSLog(@"unlockMethod unlock");
[self.condition unlock];
}NSConditionLock
Extends NSCondition with a numeric condition value, enabling ordered execution of multiple threads.
self.lock = [[NSConditionLock alloc] initWithCondition:0];
// Thread C
[self.lock lockWhenCondition:0];
// ...
[self.lock unlockWithCondition:1];
// Thread B
[self.lock lockWhenCondition:1];
// ...
[self.lock unlockWithCondition:2];
// Thread A
[self.lock lockWhenCondition:2];
// ...
[self.lock unlock];DISPATCH_QUEUE_SERIAL
A serial GCD queue can serialize access to a resource, avoiding explicit lock management.
self.serialQueue = dispatch_queue_create("com.example.serialQueue", nil);
dispatch_async(self.serialQueue, ^{ /* critical section */ });dispatch_semaphore_t
A semaphore with an initial value of 1 works as a binary lock, putting waiting threads to sleep.
self.semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// critical section
dispatch_semaphore_signal(self.semaphore);@synchronized
Provides a simple syntax‑sugar lock that internally uses objc_sync_enter and objc_sync_exit, which are built on top of a recursive pthread_mutex.
@synchronized(self) {
[self threadAction];
}Atomic Property
In Objective‑C, the atomic attribute adds a spin lock (now implemented with os_unfair_lock) around the getter and setter to ensure thread‑safe access, but it does not protect direct ivar access.
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
if (!atomic) {
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}Performance Comparison
Ranking of iOS locks from fastest to slowest (roughly): OSSpinLock, dispatch_semaphore_t, pthread_mutex_t, DISPATCH_QUEUE_SERIAL, NSLock, NSCondition, os_unfair_lock, recursive pthread_mutex, NSRecursiveLock, NSConditionLock, @synchronized. Choose a lock based on the expected contention and execution frequency.
OSSpinLock
dispatch_semaphore_t
pthread_mutex_t
DISPATCH_QUEUE_SERIAL
NSLock
NSCondition
os_unfair_lock
pthread_mutex_t (recursive)
NSRecursiveLock
NSConditionLock
@synchronized
References
Priority Inversion Explained: https://zhuanlan.zhihu.com/p/146132061
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
