Fundamentals 22 min read

Mastering iOS Locks: Types, Usage, and Performance Comparison

This article explains the relationship between locks and multithreading on iOS, introduces common lock types such as NSRecursiveLock, NSConditionLock, OSSpinLock, os_unfair_lock, pthread_mutex, and @synchronized, demonstrates resource contention with code examples, discusses lock selection, priority inversion, and provides a performance ranking of various locks.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Mastering iOS Locks: Types, Usage, and Performance Comparison

01 Introduction

Locks are tightly coupled with multithreading; in a multithreaded environment, if thread

A

accesses code protected by a lock, thread

B

must wait for thread

A

to release the lock before it can access the same code.

02 Types

Here are the main lock types:

NSRecursiveLock: a recursive lock that can be acquired repeatedly by the same thread without deadlock, tracking lock and unlock counts.

NSConditionLock: a condition lock that allows a thread to acquire the lock only when a user‑defined condition is satisfied; it has the longest overhead among the locks.

OSSpinLock: a spin lock that stays in user space, reducing context switches and offering the highest performance, but it consumes CPU while spinning; for longer waits,

pthread_mutex

is preferable.

os_unfair_lock: replaces

OSSpinLock

due to its issues.

pthread_mutex_t: a C‑level mutex with high performance at the API level.

@synchronized: a simple block‑based lock.

dispatch_semaphore: a semaphore provided by GCD.

NSLock: a high‑level mutex wrapper.

pthread_spinlock_t: a pthread spin lock (unsupported on iOS).

03 Resource Contention

When multiple threads read/write the same resource, resource contention can occur, leading to incorrect results. Locks or dispatch queues are typically used to solve this.

Example using

sleep

to simulate different thread logic:

<code>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];
    }
});

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]);
}
</code>

The log shows that each thread reads the old value, sleeps, then writes back, causing later threads to overwrite earlier calculations—a classic resource contention scenario.

<code>count: 14, thread: <NSThread: 0x600003670f00>{number = 5, name = (null)}
count: 14, thread: <NSThread: 0x600003668840>{number = 6, name = (null)} // resource contention
... (remaining log omitted for brevity)
</code>

04 Lock Classification

From the perspective of lock type:

4.1 Spin Locks

Spin locks are blind‑waiting locks that continuously consume CPU while waiting.

Example:

<code>- (void)method1 {
    lock(lock);
    // ...
    unlock(lock);
}

- (void)method2 {
    lock(lock);
    // ...
    unlock(lock);
}
</code>

Spin locks are efficient because they avoid thread wake‑up overhead, looping on

while

. For

OSSpinLock

, the implementation is a loop between

a32

and

a43

.

4.2 Mutex Locks

When a mutex lock is waiting, the thread sleeps. For example,

pthread_mutex_lock

invokes a system call to put the thread to sleep until

unlock

is called, which incurs scheduling overhead.

4.3 Recursive Locks

Recursive locks allow the same thread to lock the same object multiple times without deadlocking; the lock is released only after the number of

unlock

calls matches the number of

lock

calls.

<code>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");
}
NSLog(@"loop finished");
</code>

4.4 Selection

Choose between mutex and spin locks based on scenario. Spin locks are suitable for short‑wait, CPU‑bound tasks on multi‑core CPUs; mutex locks are better for longer, more expensive tasks.

4.5 Priority Inversion

When high‑priority thread

A

waits for a lock held by low‑priority thread

B

, and a medium‑priority thread

C

preempts

B

, priority inversion occurs. The system may temporarily raise

B

's priority to resolve the issue.

05 Which Locks to Use

5.1 OSSpinLock

OSSpinLock

is a spin lock but is deprecated on iOS;

os_unfair_lock

introduced in iOS 10 replaces it. While

OSSpinLock

offers top performance, it is no longer recommended for safety.

5.2 os_unfair_lock

os_unfair_lock

is a mutex introduced in iOS 10. Unlike spin locks, it puts waiting threads to sleep, avoiding CPU consumption.

<code>typedef struct os_unfair_lock_s os_unfair_lock;
typedef struct os_unfair_lock_s *os_unfair_lock_t;
</code>
<code>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);
</code>

5.3 pthread_mutex_t

pthread_mutex_t

is a C‑level mutex that can be configured as a normal or recursive lock.

<code>- (void)dealloc {
    pthread_mutex_destroy(&_lock);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&amp;attr);
    pthread_mutexattr_settype(&amp;attr, PTHREAD_MUTEX_DEFAULT);
    pthread_mutex_init(&_lock, &amp;attr);
    pthread_mutexattr_destroy(&amp;attr);
}

- (void)getUserInfo {
    pthread_mutex_lock(&_lock);
    // business logic
    pthread_mutex_unlock(&_lock);
}
</code>

5.4 NSLock

NSLock

is a high‑level wrapper around

pthread_mutex

, providing simple lock/unlock methods.

<code>@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

@interface NSLock : NSObject <NSLocking>
@property (nullable, copy) NSString *name;
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end
</code>

5.5 NSRecursiveLock

Allows the same thread to lock repeatedly; internally it uses

PTHREAD_MUTEX_RECURSIVE

.

5.6 NSCondition

A condition lock built on

pthread_mutex_t

and

pthread_cond

, supporting

wait

,

signal

, and

broadcast

.

<code>self.condition = [[NSCondition alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"thread:%@", [NSThread currentThread]);
    [self lockMethod];
});

- (void)lockMethod {
    [self.condition lock];
    NSLog(@"lockMethod lock");
    NSLog(@"lockMethod wait...");
    [self.condition wait];
    NSLog(@"lockMethod wait completion");
    [self.condition unlock];
    NSLog(@"lockMethod unlock");
}
</code>

5.7 NSConditionLock

Extends

NSCondition

by adding a condition value;

lockWhenCondition:

and

unlockWithCondition:

control execution order across threads.

<code>- (instancetype)initWithCondition:(NSInteger)condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
</code>

5.8 DISPATCH_QUEUE_SERIAL

A serial GCD queue can act as a lock by ensuring tasks execute one after another.

<code>self.serialQueue = dispatch_queue_create("com.sohu.serialQueue", nil);

- (void)threadActionA {
    NSLog(@"A begin");
    dispatch_async(self.serialQueue, ^{ NSLog(@"A threadExecute"); });
}
</code>

5.9 dispatch_semaphore_t

A GCD semaphore can limit concurrent access; setting its value to 1 creates a binary lock.

<code>self.semaphore = dispatch_semaphore_create(1);
for (NSInteger i = 0; i < 5; i++) {
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
    [thread start];
}

- (void)threadAction {
    NSLog(@"semaphore waiting");
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"thread:%@", [NSThread currentThread]);
    sleep(1);
    dispatch_semaphore_signal(self.semaphore);
}
</code>

5.10 @synchronized

Uses

objc_sync_enter

and

objc_sync_exit

under the hood, effectively a recursive

pthread_mutex_t

lock.

<code>@synchronized([self class]) {
    [self threadAction];
}
</code>

5.11 atomic

Objective‑C

atomic

properties generate getter/setter code that protects access with a spin lock (now implemented with

os_unfair_lock

). This adds overhead, so frequent properties should avoid

atomic

unless thread safety is required.

<code>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 &amp;slotlock = PropertyLocks[slot];
        slotlock.lock();
        *slot = newValue;
        slotlock.unlock();
    }
    objc_release(oldValue);
}
</code>

06 Lock Performance Comparison

Performance ranking (from fastest to slowest) on iOS: OSSpinLock, dispatch_semaphore_t, pthread_mutex_t, DISPATCH_QUEUE_SERIAL, NSLock, NSCondition, os_unfair_lock, pthread_mutex_t (recursive), NSRecursiveLock, NSConditionLock, @synchronized. Choose locks based on 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

07 References

Priority Inversion Explained: https://zhuanlan.zhihu.com/p/146132061

performanceiOSconcurrencymultithreadingLocksobjective-c
Sohu Tech Products
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.