OC底层原理二十九:NSLock、NSCondition、NSC
上一节对锁家族的@synchronized进行源码解析,本节将对锁家族的其他2位NSLock和NSCondition进行源码分析。
-
锁家族全家福(耗时图):
image.png
- NSLock应用与源码
- NSLock、NSRecursiveLock、@synchronized三者的区别
- NSCondition
- NSConditionLock
1. NSLock
- 测试代码
@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *testArray;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self demo];
}
- (void)demo {
NSLog(@"123");
self.testArray = [NSMutableArray array];
NSLock * lock = [[NSLock alloc] init]; // 创建
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock]; // 加锁
self.testArray = [NSMutableArray array];
[lock unlock]; // 解锁
});
}
}
@end
- 进入
NSLock,可以看到它遵循NSLocking协议:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking> { ... }
...
@end
@interface NSConditionLock : NSObject <NSLocking> { ... }
...
@end
@interface NSRecursiveLock : NSObject <NSLocking> { ... }
...
@end
@interface NSCondition : NSObject <NSLocking> { ... }
...
@end
NSLocking协议包含lock和unlock两个方法。NSLock、NSConditionLock、NSRecursiveLock、NSCondition都遵循NSLocking协议
- 现在,我们开始寻找
lock源码的出处:
方法一: 在代码
[lock lock]加锁处中,加入断点,打开debug汇编模式,一步步执行,查询源码的出处: 很遗憾,发现找不到方法二:
直接断点进不去,那我们运行到断点处后,加入lock符号断点,再运行代码,发现找到了,在Foundation库中执行的:
image.png
-
可是
Foudation库是未开源库,我们无法获取到源码。但是swift是开源语言。我们可以参考swift Foudation库。 -
打开
swift Foundation库,搜索class NSLock:
image.png
我们发现:
- 1.
init中初始化了pthread_mutex
lock和unlock实际都是调用了pthread_mutex相对于的lock和unlock函数顺便探究
NSRecursiveLock、NSCondition和NSConditionLock:
- 发现
NSRecursiveLock、NSCodition也是基于pthread_mutex封装的,但:NSRecursiveLock比NSLock多了一层递归逻辑NSCodition比NSLock多了一层pthread_con_init条件锁。NSConditionLock是在NSCondition的基础上进行的再次封装。NSRecursiveLock
NSCondition
NSConditionLock
结论:
锁必须调用init方法(new内部也调用了init方法),因为init会完成底层pthread_mutex相关锁的初始化
- 所有
遵循NSLocking协议的锁类,底层都是基于pthread_mutex锁来实现的,只是封装的深度不同。
NSLock性能接近pthread_mutex,而pthread_mutex(recursive)、NSRecursiveLock、NSCondition、NSConditionLock的耗时一个比一个高,就是由对pthread_mutex的封装深度决定的。
2. NSLock、NSRecursiveLock、@synchronized三者的区别
我们通过一个案例来进行分析和对比:
-
案例:
循环生成多个全局队列的异步线程,每个线程内声明block(testMethod)->实现block->调用block->嵌套调用block(递归调用) -
要求: 分别使用
NSLock、NSRecursiveLock、@synchronized实现读写安全
- (void)demo{
for (int i= 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);// 1. 声明
testMethod = ^(int value){ // 2. 实现Block块
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1); // 4. 嵌套调用block
}
};
testMethod(10); // 3.调用block
});
}
}
2.1 使用NSLock:
必须在Block实现前加锁,在调用后解锁:
image.png
相关实践:
调用前加锁: 死锁
image.png
- 仅在
第一次进入block时打印了一次,后面就死锁了。
(一直lock加锁,而没有unlock解锁导致的)2.
调用后加锁: 无效锁
image.png
- 打印结果
完全无序,锁的作用完全消失
(想想都知道,block都执行完了,你再上锁,有啥用,锁了一堆寂寞😂 )
- 所以如果使用
NSLock锁,必须在声明前加锁和调用后解锁,才能解决数据的读写安全问题。
💣 NSLock锁,只锁了当前线程,当我们使用异步多线程操作时,可能出现线程间相互等待,死锁的情况
2.2 使用NSRecursiveLock
-
在
声明前加锁和调用后解锁是正确的。
image.png
-
但由于它具备
递归特性,我们在block内部的递归前加锁,当前线程也打印正常,但是其他线程堵塞。
image.png
-
当我们去掉
for循环,仅保持一个异步线程,在block内部的递归前后分别加锁和解锁,打印正常:
image.png
这是因为NSRecursiveLock的递归特性。内部任务是递归持有的,所以不会死锁。
image.png
2.3 @synchronized
-
@synchronized最简单,直接将block内部代码包裹起来,就可以实现数据读写安全了
image.png
-
关于
@synchronized的内部结构,我们上一节专门分析了。 -
@synchronized能对记录被锁对象的所有线程,每个线程内部都是递归持有任务的。所以在异步多线程中,它既不用担心递归造成的锁释放问题,也不需要关心线程间的通信问题。
NSLock、NSRecursiveLock、@synchronized三者的区别
NSLock:
- 需要
手动创建和释放,需要在准确的时机进行相应操作。- 仅锁住
当前线程的当前任务,无法自动实现线程间的通讯和递归问题。
(上述NSLock代码实际上没解决递归问题,只是野蛮的在代码最外层上了一把大锁,无视递归内部层级)
NSRecursiveLock:
- 需要
手动创建和释放,需要在准确的时机进行相应操作。- 仅锁住
当前线程的所有任务,无法自动实现线程间的通讯,但可以解决递归问题。
(与NSLock不同,NSRecursiveLock是在递归时,每层加锁和解锁。对锁的控制更为精确)
@synchronized:
- 只需将
需要锁的代码都放在作用域内,确定被锁对象(被锁对象决定了锁的生命周期),@synchronized就可以做到自动创建和释放。
锁住被锁对象的所有线程的所有任务,可自动实现线程间的通讯,可以解决递归问题。
(内部逻辑为:被锁对象可持有多个线程,每个线程可递归持有多个任务)
所以我们日常使用时,尽管@synchronized耗时较大,但是它使用非常简单,根本不需要处理各种异常情况,也不需要手动释放。便捷性和安全性都非常好。
3. NSCondition
NSCondition的对象实际上是作为一个锁和一个线程检查器:
-
锁: 当检查
条件成立时,保护数据源 -
线程检查器:
根据条件判断是否继续运行线程(线程是否阻塞)
方法:
[condition lock]:加锁
(一般用于多线程同时访问、修改同一个数据源时,保证同一时间内数据源只能被访问、修改一次,其他线程的命令需要在lock外等待,只有unlock后,才可访问)[condition unlock]:解锁(与lock配对使用)[condition wait]:让当前线程处于等待状态[condition signal]:CPU发信号告诉所有线程不用再等待,可以继续执行。
- 测试案例:
有2个生产者和2个消费者,各自生产和消费各50次。当消费者购买时,没货就排队等待,有货就卖货,一次只能一个人买, 每当生产者生产出一个货物时,都会广播告诉所有等待的消费者,进行继续购买。
这样保障了货品的数据安全(有货才能卖,一次卖一个,没货就等待)
@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic, strong) NSCondition *testCondition;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 0;
[self demo];
}
- (void)demo{
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer]; // 生产者
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer]; // 消费者
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer]; // 消费者
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer]; // 生产者
});
}
}
- (void)producer{
[_testCondition lock]; // 操作的多线程影响
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal]; // 发送信号
[_testCondition unlock];
}
- (void)consumer{
[_testCondition lock]; // 操作的多线程影响
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[_testCondition wait]; // 线程等待
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
@end
-
打印结果:(生产消费数据是安全的)
image.png
但是NSCondition使用非常麻烦,需要在合适的地方手动加锁、等待、发送信号、释放
于是基于NSCondition,出现了NSConditionLock锁
4. NSConditionLock
NSConditionLock是一把锁,一旦一个线程获得锁,其他线程一定等待。
方法:
[xxx lock]:
加锁
- 如果
没有其他线程获得锁(不需要判断内部的condition),那他能执行后续代码,同时设置当前线程获得锁。- 如果已经
有其他线程获得锁(可能是条件锁,或者无条件锁),则等待,直到其他线程解锁[xxx lockWhenCondition: A条件]:
- 在
[xxx lock]的基础上,没有其他线程获得锁,且内部的condition条件满足A条件时,才会执行后续代码并让当前线程获得锁。否则依旧是等待[xxx unlockWithCondition: A条件]:
释放锁
- 把
内部的condition设置为A条件,并broadcast广播告诉所有等待的线程。return = [xxx lockWhenCondition: A条件 beforeDate: A时间]:
没有其他线程获得锁,且满足A条件,且在A时间之前,可以执行后续代码并让当前线程获得锁。- 返回值为
NO,表示没有改变锁的状态
condition是整数,内部通过整数比较条件
- 通过下面案例分析:
- (void)demo{
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1]; // conditoion = 1 内部 Condition 匹配
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0]; // 解锁并把conditoion设置为0
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2]; // conditoion = 2 内部 Condition 匹配
sleep(0.1);
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1]; // 解锁并把conditoion设置为1
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
}
-
打印结果:
image.png
分析:
- 有三个
并行队列+异步函数,分别处理三个任务,三个任务的执行顺序无序。
(并行队列+异步线程是的执行顺序是不固定的,取决于任务资源大小和cpu的调度)- 我们init时,将condition设置为2。
- 任务1: 必须当前线程
没被锁,且condition为1时,我才加锁并执行后面代码。- 任务2: 必须当前线程
没被锁,且condition为2时,我才加锁并执行后面代码。- 任务3: 必须当前线程
没被锁,我可以加锁并执行后面代码。所以
任务3的执行时期并不确定,只要当前线程没被锁,随时都可以。任务1一定在任务2的后面:
- 因为
condition初始值为2,只有任务2满足条件,任务2执行完后,将condition设置为1,并broadcast广播给所有等待的线程。- 此时
正在等待的任务1的线程收到广播,检查任务1,满足条件,任务1执行完后,将condition设置为0,并broadcast广播给所有等待的线程。
-
Swift Foundation源码中搜索NSConditionLock,可以看到循环检查线程、条件和上锁过程:
image.png
感兴趣的,我们可以
汇编验证下部分流程:
(汇编是机器执行的代码,是最准确的执行顺序。找不到源码时,只有它才是最有效的探索路径)(PS:
汇编确实很难懂,这里只是简单介绍一下部分流程,主要是思路的拓宽)
- 简化测试代码:
- (void)demo{ NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ [conditionLock lockWhenCondition:2]; // conditoion = 2 内部 Condition 匹配 NSLog(@"线程 2"); [conditionLock unlockWithCondition:1]; // 解锁并把conditoion设置为1 }); }
lockWhenCondition加上断点,打开汇编模式:image.png
image.png
运行代码,执行到断点处,再加入lockWhenCondition:符号断点:(注意:冒号不能少,前后不能有空格),再运行代码:
image.png
在
lockWhenCondition:beforeDate:一行加断点,运行至此处,读取参数,发现beforeDate的默认值是distantFuture:
image.png
加入
lockWhenCondition:beforeDate:符号断点,运行代码,进入到该函数内:
image.png
发现首先调了
lock函数,我们加入lock断点,运行代码,发现内部是NSCondition执行了lock方法:
image.png
回到上一页,我们在
pthread_equal下一行加入断点,运行代码。打印相应值:
image.png
pthread_equal检查线程是否存在,true:跳到0x7fff207ef545,false:比较r15和rbx偏移0x10位。
这里实际就是检查线程是否存在,如果不存在,再检查condition是否相等。才进行后续操作... 大概思路就是这样... 讲个思路就行。 真正的汇编探索,还需要
很大的基本功和海量训练。
关于锁的探索,到此为止。 其他类型的锁,可以用类似方式去探索和研究。
image.png
NSRecursiveLock
NSCondition
NSConditionLock
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png