前言
上一篇文章重点讲解了@synchronized
的使用以及其底层原理,其实iOS
开发中还提供了其他锁让我们使用,那么现在就开始来分析探索各种所的使用。
准备工作
1. NSLock和NSRecursiveLock的使用
首先我们引入下面的案例:
- (void)lg_testRecursive{
for (int i= 0; i<10; 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);
});
}
}
运行结果如下:
![](https://img.haomeiwen.com/i25800336/c843f929d67f6c22.png)
其中
testMethod
为静态block
,因为多线程
的影响,所以运行结果错乱无序的
,但是这个案例还是出现了混乱的过程,这是什么回事呢?又有什么解决的办法呢?请继续往下。![](https://img.haomeiwen.com/i25800336/f7d2ea40c5f64172.png)
加载递归里面:
![](https://img.haomeiwen.com/i25800336/a6284377a430b32c.png)
在分析
@synchronized
时知道,数据结构SyncData
中分装了recursive_mutex_t
递归互斥锁
,@synchronized
是一个支持多线程递归
的锁。
1.1 NSLock
使用NSLock
进行加锁,将锁加在业务代码外层
,修改后代码如下:
![](https://img.haomeiwen.com/i25800336/91d8f4609e66d978.png)
好明显 在业务代码
外层
使用NSLock
是可以的,那么在业务代码中
使用NSLock
加锁呢?请往下看:![](https://img.haomeiwen.com/i25800336/31153a42cb997406.png)
此时的输出被阻塞了,这是为什么呢?因为在调用
textMethod
方法之后,lock
加锁,内部又继续调用testMethod
,导致重复加锁
。
注意:这里说明NSLock是不支持递归加锁
!
1.2 NSRecursiveLock
同样的流程,在业务代码外部
使用NSRecursiveLock
进行加锁,如下:
![](https://img.haomeiwen.com/i25800336/aff85885dc4377ad.png)
由运行结果可以看出,使用
NSRecursiveLock
在业务代码外层加锁是可以的,那么放在业务代码内,请往下看:![](https://img.haomeiwen.com/i25800336/4fa8159334d61ab0.png)
通过上面的运行结果发现,
NSRecursiveLock
在完成一次
业务操作后就崩溃
了。
注意:以上说明NSRecursiveLock
支持单线程内的递归加锁
,但是并不支持多线程递归
。
2. NSCondition的使用
NSCondition
的对象实际上作为⼀个锁
和⼀个线程检查器
:锁主要为了当检测条件时保护数据源,执⾏条件引发的任务;线程检查器主要是根据条件决定是否继续运⾏线程,即线程是否被阻塞
。
2.1 NSCondition提供的API
-
[condition lock];
⼀般⽤于多线程
同时访问、修改同⼀个数据源,保证在同⼀时间内数据源只被访问、修改⼀次
,其他线程的命令需要在lock
外等待,只到unlock
,才可访问。 -
[condition unlock];
与lock
同时使⽤。 -
[condition wait];
让当前线程处于等待状态
。 -
[condition signal];
CPU
发信号告诉线程不⽤在等待,可以继续执⾏
。
2.2 案例分析
模拟生产和消费的需求,开启多个线程进行产品生产,同时开启多个线程进行销售产品。见下面案例:
#pragma mark **-- NSCondition**
- (void)lg_testConditon{
NSCondition *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_producer];
});
}
// 创建-消费者
for (int i = 0; i < 50; i++) {
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];
});
}
}
- (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];
}
注意:
生产线、消费线需要进行加锁处理
,以保证多线程安全
,于此同时,生产和消费之前也存在关系,比如库存数的安全!通过[_testCondition wait];
模拟库存不足,让消费窗口停止消费;用[_testCondition signal];
模拟已有库存可以消费,向等待的线程发送信号,开始执行。
运行结果如下:
![](https://img.haomeiwen.com/i25800336/1ea13c30734b4c42.png)
3. Foundation源码了解锁的封装
NSLock
、NSCondtion
、NSRecursiveLock
底层都是对pthread
的封装。它们的底层实现都封装在了OC
环境下的Foundation
框架中,由于Foundation框架不开源
,那么我们只能从它的声明部分
进行简单的分析探索。
![](https://img.haomeiwen.com/i25800336/5a8486e37cf9016c.png)
![](https://img.haomeiwen.com/i25800336/bec0284c884c9b33.png)
那么看了上面的生面,可以知道定义了一个
NSLocking
协议,并且提供了lock
和unlock
方法。所以我们使用的⽐如条件锁
,递归锁
都会有对应的lock
方法和unlock
方法。
-
NSLock
的底层分析
源码中全局搜索NSLock:
,查到NSLock
锁定义的地方,见下图:
NSLock定义
然后进入定义中的pthread_mutex_init
函数,如下:
pthread_mutex_init
得出:
lock
方法中,调用了pthread_mutex_lock
函数;unlock
方法中,调用了pthread_mutex_unlock
函数。底层就是对pthread
的封装。 -
NSRecursiveLock
的底层分析
采用相同的方式,搜索递归锁NSRecursiveLock
,如下:
NSRecursiveLock定义
然后进入pthread_mutex_init
方法,都是一样的。如下:
pthread_mutex_init
得出:
发现其也是对pthread
的封装,并且通过在init
方法中,通过pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
进行递归
设置。 -
NSCondition
的底层分析
NSCondition
底层也是对pthread
进行了封装,见下图:
NSCondition定义
NSCondition其他操作
得出:
除了进行pthread_mutex
互斥处理外,还对pthread_cond
进行了处理,同时提供了wait
、signal
、broadcase
方法。底层同样对phread
的封装。 -
NSConditionLock
的底层分析
NSConditionLock
底层没有直接操作pthread_mutex
,如下:
NSConditionLock定义
但是NSConditionLock
实现中提供了一个NSCondition
属性和一个pthread_t
属性,通过这两个属性,实现加锁
和线程
方面的相关处理。所以也可以推断出NSConditionLock
也是由phread
封装得来的。
4. NSConditionLock的使用
NSConditionLock
也是一种条件锁
,⼀旦⼀个线程获得锁,其他线程⼀定等待。
4.1 NSConditionLock相关的API
-
[xxxx lock];
表示xxx
期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition
) 那它能执⾏此⾏以下代码,如果已经有其他线程获得锁(可能是条件锁,或者⽆条件锁),则等待,直⾄其他线程解锁
。 -
[xxx lockWhenCondition:A条件];
表示如果没有其他线程获得该锁,但是该锁内部的condition
不等于A条件
,它依然不能获得锁,仍然等待。如果内部的condition
等于A条件
,并且没有其他线程获得该锁,则进⼊代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直⾄它解锁
。 -
[xxx unlockWithCondition:A条件];
表示释放锁
,同时把内部的condition设置为A条件
。 -
return = [xxx lockWhenCondition:A条件 beforeDate:A时间];
表示如果被锁定(没获得锁),并超过该时间
则不再阻塞线程
。但是注意:返回的值是NO
,它没有改变锁的状态,这个函数的⽬的在于可以实现两种状态下的处理。
4.2 案例分析
案例如下:
- (void)lg_testConditonLock{
// 创建条件锁 - 需要满足条件2,否则不执行
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];
sleep(0.1);
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
}
运行结果见下图:
![](https://img.haomeiwen.com/i25800336/888dc2ce1ffd4dff.png)
分析结果:
-
NSConditionLock
创建时,设置的条件时2
,也就是说需要满足条件2
,否则不执行
; -
线程 1
调⽤[NSConditionLock lockWhenCondition:1]
,此时因为不满⾜当前条件,所以会进⼊waiting
状态,当前进⼊到waiting
时,会释放当前的互斥锁
。 - 此时当前的
线程 3
调⽤[NSConditionLock lock:]
,本质上是调⽤[NSConditionLock lockBeforeDate:]
,这⾥不需要⽐对条件值,所以线程 3
会打印。 - 接下来
线程 2
执⾏[NSConditionLock lockWhenCondition:2]
,因为满⾜条件值,所以线程 2
会打印,打印完成后会调⽤[NSConditionLock unlockWithCondition:1]
,这个时候将value
设置为1
,并发送boradcast
, 此时线程 1
接收到当前的信号,唤醒执⾏并打印。 - ⾃此当前打印为
线程 3
->线程 2
->线程 1
; -
[NSConditionLock lockWhenCondition:]:
这⾥会根据传⼊的condition
值和Value
值进⾏对⽐,如果不相等,这⾥就会阻塞,进⼊线程池,否则的话就继续代码执⾏
。 -
[NSConditionLock unlockWithCondition:]:
这⾥会先更改当前的value
值,然后进⾏⼴播
,唤醒当前的线程
。
5. 读写锁的实现
5.1 读写锁概念的分析理解
读写锁实际是⼀种特殊的⾃旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进⾏读访问,写者则需要对共享资源进⾏写操作。
这种锁相对于⾃旋锁⽽⾔,能提⾼并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最⼤可能的读者数为实际的逻辑CPU
数。写者是排他性的,⼀个读写锁同时只能有⼀个写者或多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的
。
⼀次只有⼀个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁.
正是因为这个特性,当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.当读写锁在读加锁状态时, 所有试图以读模式对它进⾏加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进⾏加锁, 它必须直到所有的线程释放锁.
通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占⽤, ⽽等待的写模式锁请求⻓期阻塞.读写锁适合于对数据结构的读次数⽐写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁⼜叫共享-独占锁.
5.2 用到的API
-
pthread_rwlock_t lock
; // 结构 -
pthread_rwlock_init(&lock, null)
; // 初始化 -
pthread_rwlock_rdlock(&lock)
; // 读加锁 -
pthread_rwlock_tryrdlock(&lock)
; // 读尝试加锁 -
pthread_rwlock_wdlock(&lock)
; // 写加锁 -
pthread_rwlock_trywdlock(&lock)
; // 写尝试加锁 -
pthread_rwlock_unlock(&lock)
; // 解锁 -
pthread_rwlock_destory(&lock)
; // 销毁
5.3 pthread_rwlock_t的使用
引入下面的案例,开启十个线程
,同时进行读写操作, 要求:
- 可以实现多读,多读不互斥
- 单写,读写互斥
- 写写互斥
实现代码如下:
#import <Pthread.h>
@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic,assign) pthread_rwlock_t lock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 0;
[self rwTest];
}
- (void)rwTest {
// 初始化
pthread_rwlock_init(&_lock, NULL);
// 全局队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 开启读写
for (int i = 0; i<10; i++) {
dispatch_async(queue, ^{
[self read];
});
dispatch_async(queue, ^{
[self read];
});
// 写
dispatch_async(queue, ^{
[self write];
});
}
}
// 读流程
-(void)read{
// 读加锁
pthread_rwlock_rdlock(&_lock);
sleep(1);
NSLog(@"读……%zd", self.ticketCount);
// 解锁
pthread_rwlock_unlock(&_lock);
}
// 写
-(void)write{
// 写加锁
pthread_rwlock_wrlock(&_lock);
sleep(1);
NSLog(@"写……%zd", ++self.ticketCount);
// 解锁
pthread_rwlock_unlock(&_lock);
}
@end
运行结果如下:
![](https://img.haomeiwen.com/i25800336/d63588e245ff8142.png)
通过上面的案例可以反映出
读写锁同时只能有⼀个写者
,并且可以保证多读同时进行
。
5.4 GCD栅栏函数的使用来实现读写锁
实现代码如下:
#import <pthread.h>
@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
// 并发队列-多读
@property (nonatomic, strong) dispatch_queue_t qCONCURRENT;
// 串行队列-限制读取顺序
@property (nonatomic, strong) dispatch_queue_t qSERIAL;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 0;
// 队列初始化
self.qCONCURRENT = dispatch_queue_create("selfCONCURRENT", DISPATCH_QUEUE_CONCURRENT);
self.qSERIAL = dispatch_queue_create("selfSERIAL", DISPATCH_QUEUE_SERIAL);
[self go_testReadWrite];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 写 栅栏函数,保证读写的互斥
dispatch_barrier_async(self.qCONCURRENT, ^{
[self writeAction];
});
}
#pragma read wirte
- (void)go_testReadWrite{
// 多线程读
for (int i = 0; i < 2000; i++) {
dispatch_async(self.qCONCURRENT, ^{
[self readAction];
});
dispatch_async(self.qCONCURRENT, ^{
[self readAction];
});
dispatch_async(self.qCONCURRENT, ^{
[self readAction];
});
}
}
- (void)readAction {
// 保证读取顺序
dispatch_async(self.qSERIAL, ^{
sleep(1);
NSLog(@"读 ..... %ld ------ %@", self.ticketCount, [NSThread currentThread]);
});
}
- (void)writeAction {
sleep(1);
NSLog(@"写 ..... %ld ------ %@", ++self.ticketCount, [NSThread currentThread]);
}
运行结果如下:
![](https://img.haomeiwen.com/i25800336/c5c88641f1321010.png)
通过案例发现读写锁的实现还是有多种的方式的。
总结
OC
中锁的探索就到此结束了,在学习过程中收获满满。这对我们在开发过程中保证线程的安全有很大的帮助哦。
网友评论