上一节对锁家族
的@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汇编模式
,一步步执行,查询源码
的出处: 很遗憾,发现找不到方法二:
image.png直接断点
进不去,那我们运行到断点
处后,加入lock符号断点
,再运行代码,发现找到了,在Foundation库
中执行的:
-
可是
Foudation
库是未开源库
,我们无法获取
到源码
。但是swift
是开源语言
。我们可以参考swift Foudation库
。 -
打开
image.pngswift Foundation
库,搜索class NSLock
:
我们发现:
- 1.
init
中初始化了pthread_mutex
lock
和unlock
实际都是调用
了pthread_mutex
相对于的lock
和unlock
函数顺便探究
NSRecursiveLock
、NSCondition
和NSConditionLock
:NSRecursiveLock NSCondition
- 发现
NSRecursiveLock
、NSCodition
也是基于pthread_mutex
封装的,但:NSRecursiveLock
比NSLock
多了一层递归逻辑
NSCodition
比NSLock
多了一层pthread_con_init
条件锁。NSConditionLock
是在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
- 仅在
第一次
进入block
时打印了一次
,后面就死锁
了。
(一直lock
加锁,而没有unlock
解锁导致的)2.
image.png调用后
加锁: 无效锁
- 打印结果
完全无序
,锁
的作用完全消失
(想想都知道,block
都执行完
了,你再上锁
,有啥用
,锁
了一堆寂寞
😂 )
- 所以如果使用
NSLock锁
,必须在声明前加锁
和调用后解锁
,才能解决数据
的读写安全
问题。
💣 NSLock
锁,只锁了当前线程
,当我们使用异步多线程
操作时,可能出现线程
间相互等待
,死锁
的情况
2.2 使用NSRecursiveLock
-
在
image.png声明前加锁
和调用后解锁
是正确的。
-
但由于它具备
image.png递归特性
,我们在block内部
的递归前
加锁
,当前线程
也打印正常
,但是其他
线程堵塞
。
-
当我们去掉
image.pngfor循环
,仅保持一个异步线程
,在block内部
的递归前后
分别加锁
和解锁
,打印正常:
这是因为NSRecursiveLock
的递归特性
。内部任务是递归持有
的,所以不会死锁
。
2.3 @synchronized
-
image.png@synchronized
最简单,直接将block
内部代码包裹
起来,就可以实现数据读写安全
了
-
关于
@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 }); }
image.png
lockWhenCondition
加上断点
,打开汇编
模式:
image.png
image.png
运行
代码,执行到断点
处,再加入lockWhenCondition:
符号断点:(注意:冒号不能少
,前后
不能有空格
),再运行代码
:
在
image.pnglockWhenCondition:beforeDate:
一行加断点
,运行
至此处,读取参数
,发现beforeDate
的默认值是distantFuture
:
加入
image.pnglockWhenCondition:beforeDate:
符号断点,运行代码,进入到该函数内:
发现首先调了
image.pnglock
函数,我们加入lock
断点,运行
代码,发现内部是NSCondition
执行了lock
方法:
回到上一页,我们在
image.pngpthread_equal
下一行加入断点,运行代码。打印相应值:
pthread_equal
检查线程是否存在,true
:跳到0x7fff207ef545
,false
:比较r15
和rbx
偏移0x10
位。
这里实际就是检查线程是否存在,如果不存在,再检查condition
是否相等。才进行后续操作... 大概思路就是这样... 讲个思路就行。 真正的汇编探索,还需要
很大的基本功
和海量训练
。
关于锁的探索
,到此为止。 其他类型的锁,可以用类似方式
去探索
和研究
。
网友评论