本章提纲:
1、NSLock
2、NSRecursiveLock
3、NSCondition
4、NSConditionLock
5、读写锁的实现
上一篇我们了解了synchronized
的使用,这篇文章来补充其他我们平时开发中常用的一些锁的示例,以及简单的源码探索。
1.NSLock
- NSLock的基本使用
//NSLock的创建
NSLock *nlock = [[NSLock alloc] init];
//NSLock加锁
[nlock lock];
// NSLock解锁
[nlock unlock];
它的使用非常简单,下面我们来结合具体示例。
1.1 NSLock的使用示例
- 测试代码
多线程递归打印,未加锁之前
可以看到打印结果,在未经过任何临界区处理,没有加锁的情况下,打印的结果非常随意混乱。
多线程递归打印,递归外部加锁NSLock
可以看到结果变得开始有顺序了,在一次线程操作未处理完毕之前,下一个线程不能访问,所以每次线程执行递归的,都是有序的打印。
多线程递归打印,递归内部加锁NSLock
可以看到,只打印了10,程序就没有后续了,在递归内部,加了一次锁之后,
testMethod
又调用了自己,然后又走到[lock lock];
因为这段已经加了锁,所以要等着解锁才能再一次执行testMethod
,因为解锁的代码在testMethod
之后,永远也走不到,所以这里有点死锁
的状态。
通过上面三种情况的讨论,可以了解到NSLock
可以用于多线程使线程安全,起到保护临界区的作用。但是不能在递归中使用。我们后边要学习的NSRecursiveLock
是可以在递归中使用的,后面详细介绍。
1.2 NSLock源码窥探
我们打一个符号断点-[NSLock lock]
,看看NSLock的底层实现所在的框架。
可以看到NSLock
是出自于Foundation
框架,而oc
版本的Foundation
框架并没有开源,我们通过Swift
版本的来看一下里面的具体实现,打开swift-corelibs-foundation-master代码,搜索NSLock
,来到它的定义的部分。
1、我们通过这个源码可以了解到
NSLock
遵循NSLocking
,而NSLocking
是协议。
2、而NSLock的初始化
init
实际上调用的是pthread_mutex_init
,pthread_mutex_init
是pthread
框架下的一个API,所以NSLock的实际实现其实是对pthread的封装。
- pthread简介
pthread是 POSIX threads 的简称,是POSIX的线程标准。POSIX是可移植操作系统接口(Portable Operating System Interface)的简称,其定义了操作系统的标准接口,旨在获得源代码级别的软件可移植性。它是一套通用的多线程API,适用于Unix、Linux、Windows等系统,跨平台、可移植,使用难度大,C语言框架,线程生命周期由程序员管理。
从源码的部分点进去,可以看到一部分pthread的API,有初始化的,加锁,解锁的等等。
2. NSRecursiveLock
同样NSRecursiveLock
也是对pthread
的封装,具体的实现和NSLock
的差别我们在下面源码的时候说明。先来看它和NSLock
在使用上的差别。还是上面的代码做示例。
2.1 NSRecursiveLock的使用示例
多线程递归打印,递归外部加锁NSRecursiveLock
它的效果和
NSLock
是一样的。
多线程递归打印,递归内部加锁NSRecursiveLock
可以看到,一次的递归调用完成了,打印出了完整的10到1。我具体的打了个断点,看看这个递归锁在递归中是怎么执行的,
左边线程堆栈展示的invoke2比十个多,但是我们递归一共调用了十次,也就是说别的线程有的调用了lock也记到了当前线程里边去,所以在解锁的时候当前线程多调用了unlock,所以引发了崩溃。
通过这个表现我们可以看出来NSRecursiveLock
支持递归使用,但是不支持多线程!
2.2 NSRecursiveLock源码窥探
我们来到源码看到NSRecursiveLock
的实现和NSLock
很相似。
多了个attr的初始化和设置,传的参数是
PTHREAD_MUTEX_RECURSIVE
pthread_mutexattr_init(attrs)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
而且在pthread_mutex_init()
调用时,NSLock
传的是nil,而NSRecursiveLock
把初始化好的attrs
传了进去。
3. NSCondition
NSCondition
是条件锁,它的使用方式和信号量很相似,当线程符合条件的时候才会执行,否则会等待被阻塞,等待满足条件时再执行。
- API展示
//加锁
[condition lock];
//与lock同时使⽤,解锁
[condition unlock];
//使当前线程处于等待状态
[condition wait];
//CPU发信号告诉线程不⽤等待,继续执⾏
[condition signal];
3.1 NSCondition的使用示例
3.1.1生产者消费者问题简介
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
3.1.2解决这类问题的办法
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。
(上述介绍摘自百度百科)
- NSCondition简单使用案例
这里我们简单实现了下生产者生产,然后消费者消费的一个简单案例,只有生产者通知消费者可以消费的这么一个简单的实现。
(void)testConditon{
_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];
}
- 消费者来到方法
consumer
,发现如果共享区域的资源self.ticketCount
如果是0了,那么要等待,[_testCondition wait];
。如果不为0,那么进行"消费",资源数-1。 - 然后生产者调用方法
producer
,对共享资源self.ticketCount
进行+1的操作,然后发送signal
信号,通知正在等待消费的消费者,生产好了一个,可以唤醒一个等待消费的"消费者"。 - 而上述操作公共资源
self.ticketCount
的时候都需要加锁,防止多线程访问。
3.2 NSCondition源码窥探
它也是对pthread
的封装。
- init方法调用了
pthread_mutex_init
和pthread_cond_init
。 - lock调用了
pthread_mutex_lock
,unlock调用pthread_mutex_unlock
。 - wait调用
pthread_cond_wait
. - signal调用的是
pthread_cond_signal
- broadcast调用的是
pthread_cond_broadcast
4. NSConditionLock
NSConditionLock
也是条件锁,实际上是对NSCondition
的再封装。使用更加灵活。
- 相关API
//无条件锁 和NSLock没什么区别 底层都是pthread_mutex_lock 和pthread_mutex_unlock
[conditionLock lock];
[conditionLock unlock];
//有条件锁 当condition满足条件时加锁,不满足条件不执行
- (void)lockWhenCondition:(NSInteger)condition;
//有条件,解锁时 condition设置成相应的条件,符合这个条件的其他等待的线程就能执行了。
- (void)unlockWithCondition:(NSInteger)condition;
4.1 NSConditionLock使用示例
1、NSConditionLock的对象初始化条件为2,三个异步的线程,只有condition为2的条件里能执行。
2、线程1执行条件是1,所以不符合条件,不往下执行。线程2满足条件,可以执行;线程3没有条件限制,也可以执行。
3、因为都是异步而且在并发队列中,线程2优先级是DISPATCH_QUEUE_PRIORITY_LOW
,线程3是defualt默认优先级,而且线程2还睡眠了0.1,所以3大概率在2的前边执行。
4、而线程1要等着线程2执行完把condition置为1才能执行。
所以执行的顺序是3,2,1。
4.2 NSConditionLock源码窥探分析
回到源码,直接搜索NSConditionLock,实现如下。
open class NSConditionLock : NSObject, NSLocking {
//1、_cond是NSCondition()的对象,_value是Int类型。
//方法open var condition返回的就是_value。所以这个_value实际上就是初始化condition的值
internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
public convenience override init() {
self.init(condition: 0)
}
public init(condition: Int) {
_value = condition
}
//2、lock真实调用的是 open func lock(before limit: Date) -> Bool,传入的参数是固定的 .distantFuture
open func lock() {
let _ = lock(before: Date.distantFuture)
}
//3、解锁时 调用的是broadcast也就是pthread_cond_broadcast,和unlock。
open func unlock() {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_cond.broadcast()
_cond.unlock()
}
//返回的是value
open var condition: Int {
return _value
}
//4、lock(whenCondition condition: Int) 底层实际调用的是lock(whenCondition condition: Int, before limit: Date) -> Bool
//limit传入的是固定的.distantFuture
open func lock(whenCondition condition: Int) {
let _ = lock(whenCondition: condition, before: Date.distantFuture)
}
open func `try`() -> Bool {
return lock(before: Date.distantPast)
}
open func tryLock(whenCondition condition: Int) -> Bool {
return lock(whenCondition: condition, before: Date.distantPast)
}
//5、unlock(withCondition condition: Int)调用的是broadcast和unlock
open func unlock(withCondition condition: Int) {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_value = condition
_cond.broadcast()
_cond.unlock()
}
open func lock(before limit: Date) -> Bool {
_cond.lock()
while _thread != nil {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
#if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()
#endif
_cond.unlock()
return true
}
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()
//线程不为空 或者 传进来的 _value和传进来的condition不相同进入循环
while _thread != nil || _value != condition {
//解锁条件 具体看_cond.wait的实现,
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
#if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()
#endif
_cond.unlock()
return true
}
open var name: String?
}
1、_cond是NSCondition()的对象,_value是Int类型。所以这个_value实际上就是初始化condition的值。
2、lock真实调用的是 open func lock(before limit: Date) -> Bool,传入的参数是固定的 .distantFuture。
3、解锁时 调用的是broadcast也就是pthread_cond_broadcast,和unlock。
4、lock(whenCondition condition: Int) 底层实际调用的是lock(whenCondition condition: Int, before limit: Date) -> Bool,limit传入的是固定的.distantFuture。
5、unlock(withCondition condition: Int)调用的是broadcast和unlock
- wait(until limit: Date) -> Bool
open func wait(until limit: Date) -> Bool {
#if os(Windows)
return SleepConditionVariableSRW(cond, mutex, timeoutFrom(date: limit), 0)
#else
//timeOut不成立 就走else 也就是timeout为nil(timeout为nil时是date传的值不大于0) 就走else 否则跳过语句
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
//接着等待
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
#endif
}
- timeSpecFrom
public static let distantFuture = Date(timeIntervalSinceReferenceDate: 63113904000.0)
public var timeIntervalSinceNow: TimeInterval {
return self.timeIntervalSinceReferenceDate - CFAbsoluteTimeGetCurrent()
}
private func timeSpecFrom(date: Date) -> timespec? {
//date.timeIntervalSinceNow 不大于0 返回nil ,大于0 跳过这个语句。
guard date.timeIntervalSinceNow > 0 else {
return nil
}
let nsecPerSec: Int64 = 1_000_000_000
let interval = date.timeIntervalSince1970
let intervalNS = Int64(interval * Double(nsecPerSec))
return timespec(tv_sec: Int(intervalNS / nsecPerSec),
tv_nsec: Int(intervalNS % nsecPerSec))
}
结合上述代码,可以了解到wait方法,当传进来的limit再次是distantFuture时,timeSpecFrom才走guard语句中的返回,才不继续等待。传进来的不是distantFuture返回的是当前的时间,也就是大于0的值,是ture
distantFuture 从代码定义看,是一个非常大的值。
(而这个终止等待的条件,有可能是在broadcast
广播的时候,或者在unlock触发跳出while的条件,后面尝试验证一下)。
5.读写锁的实现
- 读写锁简介
读写锁,又叫共享-独占锁。从命名上来看,读写锁拥有两把锁,读锁
和写锁
。它的特点是: - 同一时间只允许一个线程对共享资源进行写操作;
- 当进行写操作的时候,同一时间其他线程都会被阻塞;
- 当进行读操作的时候,同一时间所有的写操作都会被阻塞;
- 当进行读操作的时候,同一时间其他线程可以进行读操作共享资源;
具体实现
根据上述要求,我们可以总结出来,就是写的时候,在写之前的所有操作要完毕,只能进行一个写操作。这符合GCD栅栏函数的特性,栅栏里的内容执行之前,会阻塞后续的同一队列上的任务,栅栏之前的操作执行完,才会执行栅栏里的操作。所以写放到栅栏里,然后所有的任务并发就ok了。
- (void)testReaderAndWriter{
self.testQueue = dispatch_queue_create("abc", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 50; i++) {
[self writer];
[self reader];
[self reader];
[self writer];
[self reader];
}
}
//读者
- (void)reader{
dispatch_async(self.testQueue, ^{
NSLog(@"读取剩余票数%lu",(unsigned long)self.ticketCount);
});
}
//写操作
- (void)writer{
//进行写操作
dispatch_barrier_sync(self.testQueue, ^{
self.ticketCount++;
});
}
实现结果