01
介绍
锁和多线程有密不可分的关系,多线程环境下,如果线程A
访问一段被锁保护的代码,线程B
访问时,需要等线程A
将锁释放,才可以进行访问。
02
类型
这里介绍下主要的几种锁:
NSRecursiveLock:递归锁,可以在一个线程中反复获取锁不会造成死锁,这个过程会记录获取锁和释放锁的次数来达到何时释放锁的作用。
NSConditionLock:条件锁,用户定义条件,确保一个线程可以获取满足一定条件的锁。因为线程间竞争会涉及到条件锁检测,系统调用上下切换频繁导致耗时是几个锁里最长的。
OSSpinLock:自旋锁,不进入内核,减少上下文切换,性能最高。但抢占多时会占用较多
cpu
,好点多,这时使用pthread_mutex
较好。os_unfair_lock:用来替代
OSSpinLock
的,OSSpinLock
存在的一些问题,后面会讲到。pthread_mutex_t:互斥锁,
C
语言层面的锁,底层api
性能高。@synchronized:更加简单,通过代码块的方式加锁。
dispatch_semaphore:信号量,
GCD
中提供的一套API
。NSLock:互斥锁,上层的高级封装,使用简单,无需关心底层细节处理。
pthread_spinlock_t:
pthread
层级的自旋锁,性能比pthread_mutex_t
要高。在等待的时候会进入“自旋”状态,不会进入休眠。但是,iOS
系统并不支持,所以用不了。
03
资源抢夺
在开发中往往是多线程的,如果多个线程同时对同一资源进行读写,就可能造成资源抢夺的问题,导致数据计算结果不是我们想要的结果。对于资源抢夺,一般都是通过加锁的方式来解决,或者用调度队列来解决。
下面是一个例子,来模拟资源抢夺,通过sleep
来模拟不同线程中的逻辑处理。
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]);
}
从打印结果可以看出,由于是先获取的值,休眠一段时间后,再进行计算和赋值操作。所以,导致进行计算时,使用的还是旧值,导致旧值的计算结果把其他线程的计算结果覆盖。这是资源抢夺中的一种场景。
count: 14, thread: <NSThread: 0x600003670f00>{number = 5, name = (null)}
count: 14, thread: <NSThread: 0x600003668840>{number = 6, name = (null)} // 发生了资源抢夺
count: 13, thread: <NSThread: 0x600003655080>{number = 7, name = (null)}
count: 12, thread: <NSThread: 0x600003670f00>{number = 5, name = (null)}
count: 11, thread: <NSThread: 0x600003668840>{number = 6, name = (null)}
count: 10, thread: <NSThread: 0x600003655080>{number = 7, name = (null)}
count: 9, thread: <NSThread: 0x600003670f00>{number = 5, name = (null)}
count: 8, thread: <NSThread: 0x600003668840>{number = 6, name = (null)}
count: 7, thread: <NSThread: 0x600003655080>{number = 7, name = (null)}
count: 6, thread: <NSThread: 0x600003670f00>{number = 5, name = (null)}
count: 5, thread: <NSThread: 0x600003668840>{number = 6, name = (null)}
count: 4, thread: <NSThread: 0x600003655080>{number = 7, name = (null)}
count: 3, thread: <NSThread: 0x600003670f00>{number = 5, name = (null)}
count: 2, thread: <NSThread: 0x600003668840>{number = 6, name = (null)}
count: 2, thread: <NSThread: 0x600003655080>{number = 7, name = (null)} // 发生了资源抢夺
04
锁的分类
这里先从类型的纬度,介绍下有哪些类型的锁。
4.1 自旋锁
自旋锁属于一种盲等的锁,当其他线程执行到已经加锁的代码时,就会进入持续等待,这个等待过程是持续消耗CPU
的。
例如下面代码,如果method1
中已经执行lock
方法,处于上锁的状态。执行到method2
的lock
方法时,所在的线程就会开始执行一个耗时任务,直到method1
的lock
放开锁为止。
- (void)method1 {
lock(lock);
// ...
unlock(lock);
}
- (void)method2 {
lock(lock);
// ...
unlock(lock);
}
但是,自旋锁的效率相对是很高的,因为不用进行线程唤醒的操作,而是让线程直接进入盲等状态,相当于运行了一个while
循环。以OSSpinLock
为例,自旋锁的实现是一个循环,下面a32
到a43
的部分,实际上是循环执行的。

4.2 互斥锁
互斥锁在进入等待的状态时,会进入休眠状态。
以pthread_mutex_lock
为例,其本质是调用了syscall
进入系统休眠状态,lock
后面的代码不再继续执行。直到持有lock
的线程调用unlock
,会唤醒线程继续执行,线程进行调度的过程中是有一定性能损耗的。
4.3 递归锁
递归锁允许同一个线程,对同一个lock
对象重复加锁。
普通锁在执行递归操作时,第二次对同一个lock
对象加锁,就会导致死锁。而递归锁则允许出现递归调用的情况,对同一个lock
对象递归调用两次也是被允许的。
4.4 选型
目前主流的锁是互斥锁和自旋锁,二者的选型主要看需求场景。自旋锁不涉及线程挂起和唤醒的操作,如果是等待时间比较短的场景,使用自旋锁比较合适。尤其是在多核CPU
中,被频繁调用的场景下,很适合用自旋锁。
因为自旋锁比较消耗CPU
,如果是比较耗时的任务,用互斥锁可以节省性能。
4.5 优先级反转
CPU
执行不同线程的任务,是通过时间片来进行的。实际执行的时候,由系统的时间片调度算法,来调度CPU
执行某个线程的任务,执行一段时间后,再去执行另一个线程的任务,这样来实现多线程。
锁会带来优先级反转的问题,即出现高优先级线程等待低优先级线程的执行,甚至导致死锁的问题。

这里用到一张很经典的优先级反转的图,有三条线程,线程A
优先级为10
,优先级最低,线程B
优先级15
,线程C
优先级20
,优先级最高。线程A
先获得互斥锁,线程C
也要获得互斥锁,随后C
进入等待过程。
这时,如果线程B
执行任务,就会抢占线程A
的时间片,线程A
要等线程B
执行完,才可以继续执行剩余任务并释放锁。从而,线程B
也间接影响了高优先级的线程C
的执行,形成了优先级反转。
优先级反转和自旋锁、互斥锁种类并无关系,当不同等级的线程伴随着锁出现时,理论上就会出现优先级反转的问题。在相同优先级的线程中不会出现优先级反转。避免优先级反转的方式也比较简单,就是在低优先级线程获得锁之前,将其设置为高优先级线程,释放锁后再设置回低优先级。
从操作系统的层面,有些系统已经内置了解决方案,在遇到此类优先级反转的情况时,会出让高优先级线程的优先级给让低优先级线程,保证锁内资源执行的正常。
05
都有哪些锁
5.1 OSSpinLock
OSSpinLock
的类型是自旋锁,但是由于其已经不能再保证线程安全性,所以苹果不建议使用,这里仅作为了解,苹果推荐使用iOS 10
新出来的os_unfair_lock
来替代OSSpinLock
。如果抛开安全性单说性能,OSSpinLock
是iOS
开发中性能最好的锁。
5.2 os_unfair_lock
os_unfair_lock
是一个互斥锁,从iOS 10
开始支持。和OSSpinLock
的实现机制不同,os_unfair_lock
在已经lock
的情况下,等待os_unfair_lock
的线程并不是循环,而是将线程睡眠,不会占用CPU
资源。
os_unfair_lock_t
本质上是一个结构体,属于比较底层的os
层的锁,所以苹果是建议我们用更高级的封装,例如NSLock
或者信号量。初始化os_unfair_lock_t
时,需要通过指定的方式OS_UNFAIR_LOCK_INIT
进行初始化。
typedef struct os_unfair_lock_s os_unfair_lock;
typedef struct os_unfair_lock_s * os_unfair_lock_t;
typedef struct _os_unfair_lock_s {
os_ulock_value_t oul_value;
} *_os_unfair_lock_t;
在使用os_unfair_lock
时需要注意,加锁和解锁的线程需要时同一个线程,否则会导致崩溃。
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);
5.3 pthread_mutex_t
pthread_mutex_t
是一个C
语言层面的锁,相对底层,需要自己进行内存管理。pthread_mutex_t
可以进行跨平台使用,可以创建两种类型的锁,一个是互斥锁,一个是递归锁,在初始化时指定类型。
下面是互斥锁的初始化,PTHREAD_MUTEX_DEFAULT
表示一个互斥锁。如果想创建一个递归锁,则改成PTHREAD_MUTEX_RECURSIVE
即可。需要注意的是,在dealloc
时需要将pthread_mutex_t
销毁。
- (void)dealloc {
pthread_mutex_destroy(&self->_lock);
}
- (void)viewDidLoad {
[super viewDidLoad];
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
pthread_mutex_init(&self->_lock, &attr);
pthread_mutexattr_destroy(&attr);
}
- (void)getUserInfo {
pthread_mutex_lock(&self->_lock);
/// 处理业务代码
pthread_mutex_unlock(&self->_lock);
}
除了常规使用方法外,pthread_mutex_t
还可以配合pthread_cond_wait
和pthread_cond_signal
两个函数,实现条件锁,具体和信号量的使用比较像。
5.4 NSLock
NSLock
我们平时用的比较多,是高级封装的一个互斥锁,其和NSRecursiveLock
等高级封装一样,遵循NSLocking
并实现加锁细节。根据汇编源码可以看出,NSLock
是对pthread_mutex
互斥锁的封装,使用也很简单。
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking>
@property (nullable, copy) NSString *name;
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end
需要注意的是,因为是互斥锁,连续在同一个线程中执行lock
会导致死锁,如果这个线程是主线程,则会导致界面卡死,并在一定时间后被系统kill
掉。如果这个线程是子线程,则会导致后面获得这个锁的线程,一直被卡住。
对于死锁的问题,我们有两种方案解决,一种是换成NSRecursiveLock
递归锁,另一种是在可能发生重复的lock
的地方,增加tryLock
的判断。如果发生重复加锁的情况,则返回NO
。
NSLock *lock = [[NSLock alloc] init];
// A线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
/// 业务代码
sleep(2);
[lock unlock];
});
// B线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
if ([lock tryLock]) {
[lock lock];
/// 业务代码
[lock unlock];
} else {
NSLog(@"加锁失败");
}
});
5.5 NSRecursiveLock
NSRecursiveLock
正如其名,是一个递归锁,也就是同一个线程,可以多次获取同一个锁,而不会发生死锁。如果是同一个线程,NSRecursiveLock
会记录获取的次数,当此线程的unlock
和lock
次数匹配时,才会释放资源给其他线程。
根据汇编源码可以看出,NSRecursiveLock
是一个PTHREAD_MUTEX_RECURSIVE
类型的pthread_mutex_t
的封装。用下面例子可以验证NSRecursiveLock
,其并不会发生死锁。
self.lock = [[NSRecursiveLock alloc] init];
for (NSInteger i = 0; i < 100; i++) {
[self.lock lock];
NSLog(@"加锁");
}
for (NSInteger i = 0; i < 100; i++) {
[self.lock unlock];
NSLog(@"解锁");
}
NSLog(@"循环已结束");
5.6 NSCondition
NSCondition
是条件锁,是对pthread_mutex_t
和pthread_cond
的封装。由于继承了NSLocking
协议,所以其自身已经具备了锁的功能。
在使用场景方面,正如其名,可以通过设置条件决定锁是否进入waiting
状态,如果进入waiting
状态,则需要其他broadcast
或者signal
来触发wait
的后续代码执行。
self.condition = [[NSCondition alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"thread:%@", [NSThread currentThread]);
[self lockMethod];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"thread:%@", [NSThread currentThread]);
[self lockMethod];
});
sleep(1);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"thread:%@", [NSThread currentThread]);
[self unlockMethod];
});
- (void)lockMethod {
[self.condition lock];
NSLog(@"lockMethod lock");
NSLog(@"lockMethod wait...");
[self.condition wait];
NSLog(@"lockMethod wait completion");
[self.condition unlock];
NSLog(@"lockMethod unlock");
}
- (void)unlockMethod {
[self.condition lock];
NSLog(@"unlockMethod lock");
NSLog(@"unlockMethod broadcast");
[self.condition broadcast];
sleep(1);
NSLog(@"unlockMethod unlock");
[self.condition unlock];
}
通过多次测试数据来看,如果只在unlockMethod
中执行broadcast
或signal
,并不会立刻触发wait
后的代码执行。因为其除了条件锁之外,本质上还是个互斥锁,需要unlockMethod
所在的线程释放锁之后,才会执行到wait
后的代码。
thread:<NSThread: 0x600001745200>{number = 6, name = (null)}
thread:<NSThread: 0x600001756540>{number = 3, name = (null)}
lockMethod lock
lockMethod wait...
lockMethod lock
lockMethod wait...
thread:<NSThread: 0x600001750480>{number = 7, name = (null)}
unlockMethod lock
unlockMethod broadcast
unlockMethod unlock
lockMethod wait completion
lockMethod unlock
lockMethod wait completion
lockMethod unlock
5.7 NSConditionLock
NSConditionLock
是对NSCondition
的进一步封装,NSConditionLock
的核心在于,当condition
为某个条件值时,才会触发另一个线程的lock
代码的执行。
初始化condition
时,可以传入一个值,并且可以在加锁和解锁的时候,也传入一个值。lockWhenCondition
表示当condition
符合传入的值,才执行加锁操作。unlockWithCondition
表示,解锁并传入一个condition
值,传入的值可能会触发其他的lockWhenCondition
。
下面是NSConditionLock
常用的一些方法,还有其继承的NSLocking
相关方法也是常用方法。
- (instancetype)initWithCondition:(NSInteger)condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
下面示例中创建了三个线程,并且初始化的时候传入的condition
是0
。当执行threadActionC
方法时,符合初始化时传入的value
,则C
的lock
方法被执行。随后会执行到unlock
并Condition
传入1
,执行threadActionB
的lock
内方法,直到执行到threadActionA
,执行到unlock
则锁被释放。
self.lock = [[NSConditionLock alloc] initWithCondition:0];
NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(threadActionA) object:nil];
[threadA start];
sleep(0.2);
NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(threadActionB) object:nil];
[threadB start];
sleep(0.2);
NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(threadActionC) object:nil];
[threadC start];
- (void)threadActionC {
NSLog(@"C begin");
/// 由于Condition初始值是0,此时Condition值为0才能加锁成功,成功后会继续执行后续代码
[self.lock lockWhenCondition:0];
NSLog(@"C threadExcute");
[self.lock unlockWithCondition:1];
}
- (void)threadActionB {
NSLog(@"B begin");
/// C设置Condition为1则将B解锁,并执行B的代码及unlock
[self.lock lockWhenCondition:1];
NSLog(@"B threadExcute");
[self.lock unlockWithCondition:2];
}
- (void)threadActionA {
NSLog(@"A begin");
/// B设置Condition为1则将A解锁,并执行A的代码及unlock,锁被释放出来
[self.lock lockWhenCondition:2];
NSLog(@"A threadExcute");
[self.lock unlock];
}
通过日志可以看出,NSConditionLock
很适合用来控制多条线程的执行,例如需要多条线程串行执行,并根据判断条件控制执行顺序。
A begin
B begin
C begin
C threadExcute
B threadExcute
A threadExcute
5.8 DISPATCH_QUEUE_SERIAL
通过GCD
的串行队列,也可以实现线程锁的作用。将资源代码放在串行队列中执行即可,这种方式不会出现多次lock
崩溃的问题,安全且灵活。
self.serialQueue = dispatch_queue_create("com.sohu.serialQueue", nil);
NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(threadActionA) object:nil];
[threadA start];
sleep(0.2);
NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(threadActionB) object:nil];
[threadB start];
sleep(0.2);
NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(threadActionC) object:nil];
[threadC start];
- (void)threadActionA {
NSLog(@"A begin");
dispatch_async(self.serialQueue, ^{
NSLog(@"A threadExcute");
});
}
- (void)threadActionB {
NSLog(@"B begin");
dispatch_async(self.serialQueue, ^{
NSLog(@"B threadExcute");
});
}
- (void)threadActionC {
NSLog(@"C begin");
dispatch_async(self.serialQueue, ^{
NSLog(@"C threadExcute");
});
}
5.9 dispatch_semaphore_t
通过GCD
的信号量,也可以实现锁的效果,并且信号量还可以进行wait
和signal
的控制。信号量可以指定并发执行数量,但一般为了防止资源抢夺,不会允许并发执行。实际上,如果不考虑资源抢夺的场景,semaphore
设置的value
就是并发的数量,只有其为正数才会执行wait
和signal
之间的代码。
通过下面的Demo
演示下如何防止资源抢夺,将semaphore
设置为1
,这样可以实现同时只有1
个线程访问wait
和signal
中间的资源。信号量在等待释放锁的过程中,线程也会进入休眠,让出CPU
的时间片,不会占用资源。
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);
}
下面是打印结果,semaphore
大于等于0
会执行wait
和signal
中间的代码,随后就是等下一个signal
信号的到来。
semaphore waiting
semaphore waiting
semaphore waiting
semaphore waiting
semaphore waiting
thread:<NSThread: 0x60000173e040>{number = 7, name = (null)}
thread:<NSThread: 0x60000173de40>{number = 9, name = (null)}
thread:<NSThread: 0x60000173e200>{number = 10, name = (null)}
thread:<NSThread: 0x60000173cec0>{number = 11, name = (null)}
thread:<NSThread: 0x60000173e080>{number = 8, name = (null)}
5.10 @synchronized
@synchronized
会以传入的对象为纬度,进行加锁。如果多段代码都用@synchronized
修饰,则同一个对象只允许被同时访问一次。但如果同一个类型的对象,是不同的实例,可以直接对类对象进行加锁,以保证对同一个对象加锁。如果不同的对象,会有可能导致@synchronized
传入的不同对象,执行时出现“锁失效”的情况。
@synchronized ([self class]) {
[self threadAction];
}
@synchronized
本质上就是语法糖,并没有直接的源码实现,需要用clang
对@synchronized
进行分析,可以看出来@synchronized
是由objc_sync_enter
和objc_sync_exit
实现的,由编译器将objc_sync_enter
和objc_sync_exit
插入到对应的位置,相关函数的实现可以从runtime
中的objc-sync.h
和objc-sync.m
看到。
从更底层来看,@synchronized
本质上是对pthread_mutex_t
的高级封装,由于用的是pthread_mutex_t
的递归锁,所以@synchronized
可以被递归调用。我后续会出一篇文章,详细介绍@synchronized
的深层实现,这里不过多展开。
using recursive_mutex_t = recursive_mutex_tt<DEBUG>;
class recursive_mutex_tt : nocopy_t {
pthread_mutex_t mLock;
public:
recursive_mutex_tt() : mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER) { }
void lock() {
lockdebug_recursive_mutex_lock(this);
int err = pthread_mutex_lock(&mLock);
if (err) _objc_fatal("pthread_mutex_lock failed (%d)", err);
}
}
5.11 atomic
属性的atomic
定义在runtime
源码的objc-accessors.mm
文件中,下面是setter
和getter
的核心实现。从代码实现可以看到,如果是原子性的属性,会加一个spinlock_t
自旋锁。
static inlinevoid reallySetProperty(idself, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, boolcopy, bool mutableCopy) {
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
下面是getter
的实现,有趣的是,虽然叫spinlock_t
,但其并不是自旋锁,而是由os_unfair_lock
实现,其本质是一个互斥锁。这是因为spinlock_t
原先是通过OSSpinLock
实现,但OSSpinLock
存在优先级反转的问题,后来将内部实现替换为os_unfair_lock
,但并未改spinlock_t
的名字。
id objc_getProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
需要注意的是,如果将一个属性设置为atomic
,这个关键字只是针对于属性的setter
、getter
方法,调用属性的setter
和getter
方法是原子性操作。如果我们调用这个属性的方法,或者通过成员变量的方式直接获取属性,这些操作并不受锁的保护。对于这些会导致资源抢夺的操作,最好在业务层使用lock
。
由于atomic
性能较差,定义的属性如果使用较频繁,并不适合用atomic
修饰。否则会出现大量的锁操作,导致性能下降。
06
锁的性能对比
下面是对iOS
中锁的性能对比,从排行来看,@synchronized
性能是最差的,但是这种锁使用起来遇到的问题也比较少,不会出现重复lock
的崩溃,如果执行次数很少,还是可以考虑的。个人认为,根据执行次数来选择锁,执行次数越多则越要考虑性能高的锁,然后在业务层做一些必要的安全处理。
OSSpinLock;
dispatch_semaphore_t;
pthread_mutex_t;
DISPATCH_QUEUE_SERIAL;
NSLock;
NSCondition;
os_unfair_lock;
pthread_mutex_t(recursive);
NSRecursiveLock;
NSConditionLock;
@synchronized;
07
参考资料
优先级反转那点事儿:
https://zhuanlan.zhihu.com/p/146132061