前言
之前我们分析过多线程,知道了线程之间存在资源竞争的问题,为了解决这个问题,系统推荐了各种锁
,保证当前只有一条线程对资源进行修改,从而保证数据的安全。今天外面重点看看系统提供了哪些锁
,以及它们的底层源码,具体做了哪些流程?
锁的性能
在 ibireme 的 不再安全的 OSSpinLock 一文中,有一张图片简单的比较了各种锁的加解锁性能:
上图可以看到除了OSSpinLock
外,dispatch_semaphore
和 pthread_mutex
性能是最高的。但是OSSpinLock
可能存在优先级反转
的问题,那什么是优先级反转呢?
首先我们看看正常情况下的线程调度原则👇
系统将线程分为5个不同的优先级
: background
,utility
,default
,user-initiated
,user-interactive
,高优先级
线程始终会在低优先级
线程前
执行,一个线程不会受到比它更低优先级线程
的干扰
。
但是可能存在这种现象👇(优先级反转)
如果一个低优先级的线程
获得锁
并访问共享资源,这时一个高优先级的线程
也尝试获得这个锁
,它会处于 spin lock
的忙等状态
从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间
,从而导致任务迟迟完不成、无法释放 lock
,这就会破坏了spin lock
,造成优先级反转。
因此,苹果工程师不建议使用OSSpinLock自旋锁
,而是尽量使用pthread_mutex
替换。
一、@synchronized
@synchronized
是我们最为熟悉的互斥锁,我们先来看看其底层实现流程是怎么样的。
示例👇
@synchronized (self) {
NSLog(@"123");
}
1.1 寻找入口
打断点,bt查看调用栈信息👇
然并卵,再进入汇编(Always Show Disassembly)👇
发现了两个objc_sync_enter objc_sync_exit,如果还不信,那么直接clang生成cpp验证👇
xcrun -sdk iphonesimulator clang -rewrite-objc filename
然后就是找objc_sync_enter
和 objc_sync_exit
是在哪个库?还是去到汇编中👇
注意:连续step into 2次
找到了入口,原来在库libobjc.A.dylib
中。
1.2 objc_sync_enter & objc_sync_exit
先看objc_sync_nil()
源码👇
BREAKPOINT_FUNCTION(
void objc_sync_nil(void)
);
# define BREAKPOINT_FUNCTION(prototype) \
OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
prototype { asm(""); }
就是asm("")
,确实没做任何处理!
至此,我们发现,objc_sync_enter
和 objc_sync_exit
都调用了id2data
函数,这个是重点!
1.3 id2data
在查看id2data
的源码之前,我们先看看几个重要的数据结构:
- StripedMap
- SyncList
- SyncData
为何将SyncData
设计成单向链表
这种结构?链表的优势在于插入、删除元素比普通的顺序表快
,试想一下这个场景,单个线程里可支持加多个锁,多个线程加多个锁
,加锁就好比是插入元素,如果采用顺序表,从头部开始查找,找到你想插入的位置再插入,就比较耗时了,所以采用链表的结构。
以上的分析,可以得出下面这张图👇
- 最左侧就是
StripedMap
,可以将它理解为一个哈希表
,里面的元素是SyncList
-
SyncList
是个结构体
,其中包含成员SyncData
-
SyncData
里又指向下一个SyncData
,使得SyncList
形成了一个单向链表
的结构 - 最终,
StripedMap
里的元素
就是一个个的单向链表
再回头来看id2data
以上都是寻找object关联的SyncData对象,没找到就new一个,如果找到呢,做了什么流程?那么再看看👇
- 线程栈存中找到的处理
- 缓存中找到的处理
首先看看fetch_cache
流程👇
接着来到_objc_fetch_pthread_data
找到的pthread数据是个结构体👇
里面的SyncCache👇
再来看看SyncCache里找到SyncData后的处理流程👇
1.4 done代码块的处理流程👇
至此,id2data
的流程分析完毕,大致就是:
- 查找
锁对象object
对应的SyncData对象
,并返回。 - 如果
线程栈存缓存
,object对应的pthread_data里的cacheItem缓存
,以及全局静态单向链表
里都没找到的话,那么就new一个SyncData对象
。 - 线程栈存缓存找到,就同步一下pthread_data里的cacheItem缓存,反之,也同步一下线程栈存缓存,保证
两个缓存数据的同步一致
。
但同时,要考虑场景,以enter为例:
- 第一次进来时
- 非第一次,同一个线程时
如果支持线程栈存👇
不支持线程栈存👇
- 非第一次,不同线程时
然后去到done代码块
,进行缓存处理
。
最后回头来看@synchronized
的底层源码,就清晰多了,找锁对象object对应的SyncData对象
,调用该SyncData对象
的成员recursive_mutex_t互斥锁
完成加锁,并且
加锁支持
可重入(lockCount++),
多个线程嵌套加(threadCount++)`。
然后看解锁
,其实与加锁
的查找流程
是一模一样
的,只是最后处理的是lockCount--
和threadCount--
👇
二、NSLock & NSRecursiveLock
2.1 NSLock
使用方式很简单,lock unlock即可👇
NSLock *lock = [[NSLock alloc] init];
[lock lock];
NSLog(@"123");
[lock unlock];
接着我们看看底层的实现👇
老规矩,还是找入口。
-
打断点,看汇编,尝试step into👇
step into根本进不去,放弃!
-
lldb 查看bt👇
还是没找到有用的信息,放弃!
- 下符号断点lock👇
发现是Foundation,但是是闭源的。依旧放弃!
- 最终,还有一个渠道,就是看swift版本的源码,因为是开源的👇
2.2 NSRecursiveLock
我们先看一个嵌套block的案例
- (void)lg_testRecursive{
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}
}
运行👇
数据会有重复,说明数据不安全!此时必须加锁,保证线程写数据的安全,
尝试用NSLock
👇
直接阻塞了,根本没起到任何作用,因为这是一个递归
,同时又是异步并发
,是个多线程递归的调用,那么就会出现场景:线程之间会出现相互等待的情况
。具体来说就是,线程1上锁后读取value值,还没解锁,此时线程2又进来加锁读取value值,线程2的任务作为了线程1的一个子任务,于是线程1的完成依赖线程2执行完成,但是线程2要执行完,必须等线程1解锁。
应对上述的场景,系统提供了一个递归锁NSRecursiveLock
,专门应对这种递归情况,使用如下👇
接下来,我们看看递归锁的底层源码(还是swift开源版本)👇
与NSLock源码对比,发现lock
和 unlock
方法是一样的,但仔细看初始化,是有些区别的👇
对当前互斥锁,做了一个PTHREAD_MUTEX_RECURSIVE
类型的设置,这是个递归的类型,而NSLock
却没有,说明是默认的类型。
但是递归锁的使用,不是很好,在哪里lock
,又在哪里unlock
,很容易出错。👇
这就是对锁的使用不熟练导致的,加锁解锁
的对象
--> 重点在于你要执行的任务
,在执行任务前
加锁,任务执行完成后
解锁。
如果不熟练,不如使用@syncronize
,一个代码块搞定,根据我们底层的分析,它更试用于多线程的场景,lockCount++保证锁可重入,threadCount++保证多线程递归👇
三、NSCondition & NSConditionLock
3.1 NSCondition条件变量
NSCondition
的对象实际上作为一个锁
和一个线程检查器
。
-
锁
主要为了当检测条件时保护数据源,执行条件引发的任务; -
线程检查器
主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
对于NSCondition条件变量
,有一个经典案例:生产消费者模型
声明 + 初始化👇
@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic, strong) NSCondition *testCondition;
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.ticketCount = 0;
[self lg_testConditon];
}
生产者👇
- (void)lg_producer{
[_testCondition lock];
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal];
[_testCondition unlock];
}
消费者
- (void)lg_consumer{
// 线程安全
[_testCondition lock];
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
// 保证正常流程
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
调用👇
- (void)lg_testConditon{
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_producer];
});
}
}
run👇
问题来了,感觉使用条件变量有些麻烦,除了lock
unlock
之外,还要一会signal
,一会wait
,根本不好控制,对于不熟练的开发者来说,很容易写错地方,导致crash。应对这样的情况,系统给我们提供了另一个锁NSConditionLock条件锁
。
3.2 NSConditionLock条件锁
相关概念
-
NSConditionLock
条件锁,一旦一个线程获得锁,其他线程一定等待。 -
[xxxx lock];
表示xxx 期待获得锁
,如果没有其他线程获得锁
(不需要判断内部的 condition) 那它能执行此行以下代码
,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁)
,则等待
,直至其他线程解锁。 -
[xxx lockWhenCondition:A条件];
- 如果
没有其他线程获得该锁
,但是该锁内部的condition不等于A条件
,它依然不能获得锁,仍然等待
。 - 如果内部的
condition等于A条件
,并且没有其他线程获得该锁
,则进入代码区,同时设置它获得该锁,其他任何线程
都将等待
它代码的完成,直至它解锁。
- 如果
-
[xxx unlockWithCondition:A条件];
表示释放锁
,同时把内部的condition设置为A条件
。 -
return = [xxx lockWhenCondition:A条件 beforeDate:A时间];
表示如果被锁定(没获得锁),并超过该时间
则不再阻塞
线程。但是注意:返回的值是NO,它没有改变锁的状态
,这个函数的目的在于可以实现两种状态下的处理
。 - 所谓的condition就是整数,内部通过整数比较条件。
案例分析
- (void)lg_testConditonLock{
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
}
run👇
分析:
-
线程1
调用[NSConditionLock lockWhenCondition:]
,因为不满足当前条件
,所
以会进入waiting 状态
,会释放当前的互斥锁。线程2
同理。 - 此时当前的
线程3
调用[NSConditionLock lock:]
,本质上是调用[NSConditionLock lockBeforeDate:]
,这里不需要比对条件值,所以线程3被优先打印
。 - 接下来
线程2
执行[NSConditionLock lockWhenCondition:]
,此时满足条件值,所以线程 2
被打印,打印完成后会调用[NSConditionLock unlockWithCondition:]
,这个时候将
value 设置为 1,并发送 boradcast,此时线程1
接收到当前的信号,唤醒执行并打印。
所以,当前打印为 线程 3 -->线程 2 --> 线程 1
。
条件锁底层源码
下面,我们依旧看看swift版的NSConditionLock的源码👇
接着看看[NSConditionLock lockBeforeDate:]
源码👇
最后看看unlockWithCondition:
源码👇
总结
开篇我们举出了日常使用的各种锁的性能对比,然后重点分析了@synchronize
的底层原理,然后通过多线程递归示例
的演示,接着分析了NSLock
和递归锁NSRecursiveLock
的底层,最后通过经典的生产消费者
模型示例,分析了条件变量NSCondition
和 条件锁NSConditionLock
的底层源码处理流程。
补充:读写锁
读写锁
适合于对数据结构的读次数比写次数多得多
的情况。 因为读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁
。
#include <pthread.h>
// 成功则返回0, 出错则返回错误编号
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
同互斥量以上, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy
对读写锁进行清理工作, 释放由init分配的资源。
// 获取读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 获取写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 释放锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
注意:获取锁的两个函数是阻塞操作。
当然,那有没有非阻塞的函数👇
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.
读写锁示例
请参考XFReadWriteLocker,不对的地方请不吝赐教!
网友评论