准备工作
一、锁的分类
互斥锁
互斥锁有互斥
和同步
两条特性,互斥性:当多条线程同时处理一个任务时,一条线程在处理任务,其他线程都不能再进行处理,只有该任务结束运行后才可以运行。同步性:首先要满足互斥性,还要有执行的顺序。常用的有:
- @synchronized
- NSLock
- pthread_mutex
自旋锁
自旋锁相当于互斥锁
+忙等
,当其检测到资源不可用时,会保持一种忙等
的状态,直到获取该资源。它的优势在于避免了上下文的切换,非常适合于堵塞时间很短的场合。缺点则是在忙等
的状态下会不停的检测状态,会占用cpu
资源。常用的有:
OSSpinLock
atomic
条件锁
通过一些条件来控制资源的访问,当然条件是会发生变化的。常用的有:
NSCondition
NSConditionLock
信号量
是一种高级的同步机制。互斥锁可以认为是信号量取值0/1时的特例,可以实现更加复杂的同步。常用的有:
dispatch_semaphore
递归锁
它允许同一线程多次加锁,而不会造成死锁。递归锁是特殊的互斥锁,主要是用在循环或递归操作中。常用的有:
pthread_mutex(recursive)
NSRecursiveLock
读写锁
是并发控制的一种同步机制,也称“共享-互斥锁”,也是一种特殊的自旋锁。它把对资源的访问者分为读者和写者,它允许同时有多个读者访问资源,但是只允许有一个写者来访问资源。常用的有:
pthread(rwlock)
dispatch_barrier_async / dispatch_barrier_sync
二、NSLock 和 NSRecursiveLock 的分析
先看下面的示例代码,外层是for
循环,内层是block
的递归调用:
- (void)ssl_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);
});
}
}
查看执行结果:
- 可以看到
value
值是乱序打印的,如果我们想要顺序打印,就要给代码加锁,那么该怎么加锁呢。
NSLock 示例
在testMethod(10);
的执行前后加上NSLock
锁:
- 没有问题,可以顺序打印。
有的时候,我们喜欢把加锁和业务代码写在一起,也就是在testMethod
的block
中加锁:
- 只打印了一个
10
,为什么呢,因为这是个递归函数,会递归加锁,而NSLock
是非递归锁,所以无法正常执行。
NSRecursiveLock 示例
NSRecursiveLock
是递归锁,我们把NSLock
换成NSRecursiveLock
,运行程序:
- 程序从
10
到1
只顺序打印了1
次,这又是为什么呢,因为NSRecursiveLock
只是递归锁,可以解决递归性
但是解决不了多线程性
,那么既要满足递归性
又要满足多线程性
就需要用到我们 上一篇 分析的@synchronized
。
@synchronized 示例
使用@synchronized
,程序正常打印:
三、NSCondition 的分析
简介
SCondition
是一个条件锁
,在日常开发中使用较少,与信号量有点相似:线程需要满足条件才会往下走,否则会堵塞等待,直到条件满足,是经典的生产消费者模型
。
NSCondition
的对象实际上作为一个锁
和一个线程检查器
:
锁
主要为了当检测条件时保护数据源,执行条件引发的任务
。线程检查器
主要是根据条件决定是否继续运行线程
,即线程是否被阻塞
。
相关函数
//一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
[condition lock];
//与lock 同时使用
[condition unlock];
//让当前线程处于等待状态
[condition wait];
//CPU发信号告诉线程不用在等待,可以继续执行
[condition signal];
生产消费者模型 示例
- (void)viewDidLoad {
[super viewDidLoad];
[self ssl_testConditon];
}
- (void)ssl_testConditon {
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self ssl_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self ssl_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self ssl_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self ssl_producer];
});
}
}
- (void)ssl_producer {
[_testCondition lock]; // 操作的多线程影响
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal]; // 信号
[_testCondition unlock];
}
- (void)ssl_consumer {
[_testCondition lock]; // 操作的多线程影响
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
执行结果:
生产一个 现有 count 1
消费一个 还剩 count 0
等待 count 0
等待 count 0
生产一个 现有 count 1
消费一个 还剩 count 0
生产一个 现有 count 1
消费一个 还剩 count 0
等待 count 0
生产一个 现有 count 1
...
- 生产者生产,消费者消费,
消费者
消费生产者
生产的,如果不能生产就不能消费。 - 生产者和消费者,本身要是安全的要进行加锁,它们之间又有联系,需要信号的控制,消费为
0
时要进行等待
,生产有了以后不能让消费一直等待,要发送信号
消费者就可以结束等待继续执行。
四、foundation 源码关于锁的封装
我们看下NSLock
的定义:
lock
和unlock
是NSLocking
的协议方法,是一个锁的协议,很多锁都有这两个方法,接下来结合swift-corelibs-foundation
源码分析NSLock
到底是怎么实现的。
NSLock 源码分析
查看NSLock
源码:
- 可以看到
NSLock
源码中确实是实现了NSLocking
协议,初始化时,本质上是对pthread_mutex
进行的封装,调用的都是pthread_mutex
的函数。
查看lock
、unlock
的实现:
- 可以看到
lock
和unlock
也是对pthread_mutex
进行的封装。
NSRecursiveLock 源码分析
查看NSRecursiveLock
源码:
- 可以看到
NSRecursiveLock
也实现了NSLocking
协议,初始化也是用pthread_mutex
进行的封装,不同之处在于NSRecursiveLock
中有设置PTHREAD_MUTEX_RECURSIVE
这个type
,这里就决定了它是个递归锁
。
NSRecursiveLock
的lock
、unlock
的实现和NSLock
基本是一样的,都是对pthread_mutex
的封装:
NSCondition 源码分析
查看NSCondition
源码:
NSCondition
也实现了NSLocking
协议,初始化也是用pthread_mutex
进行的封装,区别就在于封装了cond
的相关操作,使它拥有了wait
、signal
等功能。
lock
和unlock
也是对pthread_mutex
的封装:
五、NSConditionLock 分析
NSConditionLock 示例
看下面的示例代码及执行结果:
- (void)ssl_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];
sleep(0.1);
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
//
}
执行结果:
线程 3
线程 2
线程 1
线程 2
一定在线程 1
之前先执行,因为我们初始化conditionLock
时指定的condition
等于2
,所以lockWhenCondition:2
可以正常执行,unlockWithCondition:1
以后lockWhenCondition:1
才能执行。线程 3
中只是普通的lock
所以是可以正常执行,线程 2
和线程 3
本来是乱序的,但我们在线程 2
加了sleep(0.1)
延迟操作,所以线程 3
先执行。
NSConditionLock 源码分析
初始化源码分析
打开源码,查看NSConditionLock
的初始化:
- 通过
NSCondition()
初始化了成员变量_cond
。 - 初始化
_value
成员变量为0
。
lockWhenCondition: 源码分析
查看lockWhenCondition:
源码:
继续进入whenCondition:before:
:
- 当
_value != condition
时进行while
死循环,循环中判断等待是否超时,如果超时了返回false
,如果等到_value == condition
向下执行返回true
,任务正常执行。
unlockWithCondition: 源码分析
unlockWithCondition:
:
- 更新
_value
值为传进来的condition
。 _cond.broadcast()
进行广播通知,通知等待中的任务_value
值的改变。
六、读写锁
读写锁 概念
读写锁实际是一种特殊的互斥锁
,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性
,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU
数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU
数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。- 如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
- 一次
只有一个线程
可以占有写模式
的读写锁, 但是可以有多个线程
同时占有读模式
的读写锁,正是因为这个特性,当读写锁是写加锁状态
时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞
。 - 当读写锁在
读加锁状态
时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁。 - 通常,当读写锁处于
读模式锁住状态
时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占用, 而等待的写模式锁请求⻓期阻塞。 - 读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫
共享-独占锁
。
总结一下就是:
- 多读单写
- 写 和 写互斥
- 写 和 读互斥
- 读、写 不能阻塞任务执行
栅栏函数 实现读写锁
@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t ssl_safeQueue;
@property (nonatomic, strong) NSMutableDictionary *mDict;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self ssl_safeSetter:@"111" time:4];
[self ssl_safeSetter:@"222" time:3];
[self ssl_safeSetter:@"333" time:2];
[self ssl_safeSetter:@"444" time:1];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (int i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"读取情况:%@ -- %d -- %@",[self ssl_safeGetter],i,[NSThread currentThread]);
});
}
}
- (void)ssl_safeSetter:(NSString *)name time:(int)time {
dispatch_barrier_async(self.ssl_safeQueue, ^{
sleep(time);
[self.mDict setValue:name forKey:@"ssl"];
NSLog(@"写情况:%@ - %@",self.mDict[@"ssl"],[NSThread currentThread]);
});
}
- (NSString *)ssl_safeGetter {
__block NSString *result;
dispatch_sync(self.ssl_safeQueue, ^{
result = self.mDict[@"ssl"];
});
return result;
}
- (dispatch_queue_t)ssl_safeQueue
{
if (!_ssl_safeQueue) {
_ssl_safeQueue = dispatch_queue_create("ssl", DISPATCH_QUEUE_CONCURRENT);
}
return _ssl_safeQueue;
}
- (NSMutableDictionary *)mDict
{
if (!_mDict) {
_mDict = [NSMutableDictionary new];
}
return _mDict;
}
@end
写操作
中我们使用dispatch_barrier_async
函数,使用这个函数前面的任务执行完才会执行这里,这里执行完才会执行后面的任务,这样可以达到写和写互斥
的目的。读操作
也是放在了self.ssl_safeQueue
中,这样读和写互斥
就完成了,也就真正意义的达成单写
的目的,注意这里的dispatch_sync
只是阻塞当前任务的线程,否则result
就获取不到值了。- 因为
self.ssl_safeQueue
是并发
队列,所以多读
也是没问题的。