前言
开发中引入了异步和多线程的来提高程序性能,也就意味着线程安全成为了多线程的一个障碍,因此线程锁应运而生,而锁如果用不好,还会造成死锁的风险
下面就介绍ios中常用的几种锁,以及读写锁的实现
常见的多线程锁
ios中常见的几种锁包括OSSpinLock、信号量(Semaphore)、pthread_mutex、NSLock、NSCondition、NSConditionLock、pthread_mutex(recursive)、NSRecursiveLock、synchronized
如下所示,为前辈们测试锁性能的案例图(实际可能会略有偏差):
由于OSSpinLock目前已经不再安全,这里就放弃介绍,案例也把他给删了😂
我们再选锁的时候,如果只是使用互斥锁的效果,那么按照性能排序选择靠前的即可,如果需要锁的一些其他功能,那么根据需要选择,不必过于局限于性能,毕竟实现功能与项目的维护也是非常重要的
其他锁的使用如下所示
信号量(semaphore)
信号量实现加锁功能与其他的略有不同,其通过一个信号值来决定是否阻塞当前线程
wait操作可以使得信号量值减少1,signal使得信号量值增加1
当wait操作使得信号量值小于0时,则所在线程阻塞阻塞休眠,使用signal使得信号量增加时,会顺序唤醒阻塞线程,以此便可以实现加锁功能,
- (void)semaphore {
_semaphore = dispatch_semaphore_create(1);
}
//wait操作可以使得信号量值减少1,signal使得信号量值增加1
//当信号量值小于0时,则所在线程阻塞休眠,使用signal使得信号量增加时,会顺序唤醒阻塞线程
- (void)semaphoreUpdate {
//wait 可以理解为加锁操作,信号值小于0会休眠当前wait所在线程
//第二个参数 forever 为永远,可以自行设置一段超时时间,达到等待时间会自动解锁
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
wait和singnal中间的这部分代码,即为线程安全代码
_money++;
//signal 可以解锁
dispatch_semaphore_signal(_semaphore);
}
pthread互斥锁
pthread互斥锁是 pthread
库中的一员,linux
系统中中常用的库,使用时需要手动import导入 #import <pthread/pthread.h>
其中有 pthread_mutex_trylock
为尝试加锁,如果没被加锁,则会加锁成功,并返回0,适用于一些优先级比较低,间歇性调用的功能
注意:其他部分锁也有trylock
这个功能,例如 NSLock、NSRecursiveLock、NSConditionLock
#pragma mark --pthread互斥锁
- (void)pthreadMutex {
pthread_mutex_init(&_pMutexLock, NULL);
//使用完毕后在合适的地方销毁,例如dealloc
// pthread_mutex_destroy(&_pMutexLock);
}
- (void)pthreadMutexUpdate {
//加锁代码区间操作,避免多线程同时访问
pthread_mutex_lock(&_pMutexLock);
_money++;
//解锁代码区间操作
pthread_mutex_unlock(&_pMutexLock);
}
- (void)pthreadMutexSub {
//减少数值
[NSThread detachNewThreadWithBlock:^{
//数量大于100开始减少,假设是需要清理东西,这里减少数值
while (self->_money > 10000) {
//尝试加锁,如果能加锁,则加锁,返回零,否则返回不为零的数字
//加锁失败休眠在执行,避免抢夺资源,此任务优先级间接降低
//其他的一些锁也有这功能,例如NSLock、NSRecursiveLock、NSConditionLock
if (pthread_mutex_trylock(&self->_pMutexLock) == 0) {
self->_money--;
//解锁
pthread_mutex_unlock(&self->_pMutexLock);
}else {
[NSThread sleepForTimeInterval:1];
}
}
}];
}
NSLock互斥锁
NSLock 遵循 NSLocking协议
,是常见的互斥锁之一,为 OC 框架中的 API,使用方便,据说是 pthread 封装的锁
tryLock
方法也是尝试加锁,成功返回true,失败返回false
lockBeforeDate:(NSDate *)limit
在一个时间之间加锁,可以理解为加锁日期截止到指定时间,会自动解锁(与信号量的等待功能一样,这个是设置到指定时间)
#pragma mark --NSLock互斥锁
- (void)NSLock {
_lock = [[NSLock alloc] init];
}
- (void)NSLockUpdate {
//加锁代码区间,避免多线程同时访问
[_lock lock];
_money++;
//解锁代码区间
[_lock unlock];
}
NSCondition锁
NSCondition 算是一个稍微重量级的锁了,我理解为情景锁
(另一个原因区分条件锁 NSConditionLock
),适用于一些特殊场景,其也遵循 NSLocking
协议,也属于互斥锁
并且再其基础上,新增了信号量功能 wait
和 signal
,即 等待 和 释放 ,使用方式和 semaphore
一样,可以通过信号量控制线程的阻塞和释放,除此之外,还多了一个broadcast
,其可以解除所有因 wait 阻塞的线程
如下所示,使用 NSCondition
实现了一个生产者和消费者的案例(生产者和消费者都是同一拨人,因此需要加锁来实现,而为了保证有钱了立刻买自己想买的东西,使用信号量,保证没钱时阻塞等待,有钱时立即解放买买买)
其相当于同时使用了NSLock 和 Semaphore
功能
#pragma mark --情景锁NSCondition实现了NSLocking协议,支持默认的互斥锁lock、unlock
- (void)NSCondition {
_condition = [[NSCondition alloc] init];
}
//情景锁还加入了信号量机制,wait和signal,可以利用其完成生产消费者模式的功能
//生产者: 妈爸挣了一天的钱,储蓄值增加
- (void)conditionPlusMoney {
[_condition lock];
//信号量增加,有储蓄了,可以开放花钱功能了
if (_money++ < 0) {
[_condition signal]; //释放第一个阻塞的线程
//[_condition broadcast]; //释放所有阻塞的线程
}
[_condition unlock];
}
//消费者,服务有储蓄,拿到钱时立即解锁花钱技能(money--)
- (void)conditionSubMoney {
[_condition lock];
if (_money == 0) {
//信号量减少阻塞,打算买东西,却没钱了,停止花钱,等发工资再买东西
[_condition wait];
}
//由于之前的wait,当signal解锁后,会走到这里,开始购买想买的东西,储蓄值--
_money--;
[_condition unlock];
}
NSConditionLock
NSConditionLock 被称为条件锁,其遵循 NSLocking
协议,即具备正常的互斥锁功能
此外加入了 条件语句,为其核心功能,即满足指定条件才会解锁,因此算是一个重量级的锁了,其同时可以理解为 NSCondition 进化版
,如果你理解了 NSCondition
的生产者-消费者
模式,这个也会马上就明白了其原理了
lockWhenCondition:(NSInteger)condition
: 加锁,当条件condition为传入的condition时,方能解锁
unlockWithCondition:(NSInteger)condition
: 更新condition的值,并解锁指定condition的锁
下面使用一个异步队列,来实现类似 NSOperation 设置的依赖关系,如下所示(打印结果1、4、3、2):
#pragma mark --条件锁NSConditionLock,实现了NSLocking协议,支持默认的互斥锁lock、unlock
- (void)NSConditionLock {
_conditionLock = [[NSConditionLock alloc] initWithCondition:1]; //可以更改值测试为0测试结果
//加锁,当条件condition为传入的condition时,方能解锁
//lockWhenCondition:(NSInteger)condition
//更新condition的值,并解锁指定condition的锁
//unlockWithCondition:(NSInteger)condition
}
//多个队列执行条件锁
//通过案例可以看出,通过条件锁conditionLock可以设置线程依赖关系
//可以通过GCD设置一个具有依赖关系的任务队列么
- (void)NSConditionLockUpdate {
//创建并发队列
dispatch_queue_t queue =
dispatch_queue_create("测试NSConditionLock", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
if ([self->_conditionLock tryLockWhenCondition:1]) {
NSLog(@"第一个");
//默认初始conditon位1,所有能走到这里
//然后解锁后,并设置初始值为4,解锁condition设定为4的线程
[self->_conditionLock unlockWithCondition:4];
}else {
[self->_conditionLock lockWhenCondition:0];
NSLog(@"第一个other");
[self->_conditionLock unlockWithCondition:4];
}
});
//由于开始初始化的conditon值为1,所以后面三个线程都不满足条件
//锁定后直到condition调整为当前线程的condition时方解锁
dispatch_async(queue, ^{
//condition设置为3后解锁当前线程
[self->_conditionLock lockWhenCondition:2];
NSLog(@"第二个");
//执行完毕后解锁,并设置condition为1,设置初始化默认值,以便于下次使用
[self->_conditionLock unlockWithCondition:1];
});
dispatch_async(queue, ^{
//condition设置为3后解锁当前线程
[self->_conditionLock lockWhenCondition:3];
NSLog(@"第三个");
//执行完毕后解锁,并设置condition为3,解锁3
[self->_conditionLock unlockWithCondition:2];
});
dispatch_async(queue, ^{
//condition设置为4后解锁当前线程
[self->_conditionLock lockWhenCondition:4];
NSLog(@"第四个");
//执行完毕后解锁,并设置condition为3,解锁3
[self->_conditionLock unlockWithCondition:3];
});
}
上面的流程可以大致简化为下面几步:
1.创建一个异步队列,以便于添加后续的任务依赖
2.逐步添加子任务模块,分别在不同线程中,其有明确的依赖关系,即执行顺序为 1、4、3、2
3.使用 lockWhenCondition:
开始设置依赖,将其任务解锁的条件condition
设置为其特有的condition 号
,以便于解锁
4.执行任务时,如果 NSCondition 中的 condition 参数
,与本线程设置的tCondition
不一样时,阻塞线程,等待 NSCondition 中的 condition
更改为指定值(通过 unlockWithCondition:
更改condition值)解锁
即:默认初始化 condition 为 1,只有 任务1
能够执行,当 任务1
执行 unlockWithCondition:4
时,condition被设置为4, 阻塞的任务4
解锁,同理,任务4
执行完毕后,将 condition 设置为 3 ,任务三解锁,依次类推
5.最终根据设置的依赖关系,分别执行 任务1、任务4、任务3、任务2
pthread_mutex(recursive)
其为基于 pthread框架
的递归锁,也是以 pthread互斥锁为基础
实现的 递归锁
,即:同一个线程下,递归调用时加锁,不会阻塞当前线程,当另一个线程到来时,会因为第一个线程加的锁而阻塞
#pragma mark --pthread递归锁
- (void)pthreadMutexRecursive {
//初始化锁的递归功能
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
//互斥锁初始化时,绑定递归锁功能模块
pthread_mutex_init(&_pMutexLock, &attr);
//使用完毕后在合适的地方销毁,例如dealloc
// pthread_mutexattr_destroy(&attr);
// pthread_mutex_destroy(&_pMutexLock);
}
//使用递归锁,递归地时候回不停加锁,如果使用普通的锁早已经形成死锁,无法解脱
//递归锁的存在就是在同一个线程中的锁,不会互斥,只会互斥其他线程的锁,从而避免死锁
- (void)pthreadMutexRecursiveUpdate {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^recursiveBlock)(double count);
recursiveBlock = ^(double count){
pthread_mutex_lock(&self->_pMutexLock);
if (count-- > 0) {
self->_money++;
recursiveBlock(count);
}
pthread_mutex_unlock(&self->_pMutexLock);
};
recursiveBlock(1000);
});
}
NSRecursiveLock递归锁
和 pthread_mutex(recursive)
一样,NSRecursiveLock 也是递归锁,其遵循 NSLocking
协议,即除了递归锁功能,还具备正常的互斥锁功能
使用方式和 pthread_mutex(recursive)
一样如下所示
//使用递归锁,递归地时候回不停加锁,如果使用普通的锁早已经形成死锁,无法解脱
//递归锁的存在就是在同一个线程中的锁,不会互斥,只会互斥其他线程的锁,从而避免死锁
- (void)NSRecursiveLockUpdate {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^recursiveBlock)(double count);
recursiveBlock = ^(double count){
[self->_recursive lock];
//tryLock就不多介绍了,和Pthread的类似,注意返回值即可
//[self->_recursive tryLock];
if (count-- > 0) {
self->_money++;
recursiveBlock(count);
}
[self->_recursive unlock];
};
recursiveBlock(1000);
});
}
复制代码
synchronized
synchronized 同步锁,即同步执行,以此避免多线程同时操作同一块代码,基本上在各个平台都会有其身影,虽然效率最低,但由于使用使用简单,深得大家喜爱
实现如下所示
#pragma mark --同步锁synchronized
- (void)synchronized {
//使用简单,直接对代码块加同步锁,此代码不会被多个线程直接执行
//可以间接理解为里面的任务被放到了一个同步队列依次执行(实际实现未知)
@synchronized (self) {
self->_money++;
}
}
读写锁
读写锁
又被称为 rw锁
或者 readwrite锁
,在 ios开发中虽能见到,但确不是最常用的(一般是数据库操作才会用到)。
具体操作为:多读单写
,即,写入操作只能串行执行,且写入时,不能读取,而读取需支持多线程操作,且读取时,不能写入
相信大家也遇到过这样的事,系统的属性设置了 auto
参数,字面意思为原子性操作,其实际未能保证属性字段的多线程安全(由于旧值的赋值未加锁,同时写入时,会造成对象旧地址多次被release)
因此无论是想了解其实现方式,还是开发备用,都是有比较学习的
实现方式这里就提供两种:pthread、GCD的barrier来实现
pthread读写锁
使用前,需要先导入 pthread框架
, 即 #import <pthread/pthread.h>
实现简单,可以根据自己程序需要,选择锁初始化的合适位置
//初始化pthread读写锁
- (void)setupPhreadRW {
pthread_rwlock_init(&_lock, NULL);
//使用完毕销毁读写锁
//pthread_rwlock_destroy(&_lock);
}
#pragma mark --通过pthread读写锁来设置
- (void)setLock1:(NSString *)lock1 {
pthread_rwlock_wrlock(&_lock);
_lock1 = lock1;
pthread_rwlock_unlock(&_lock);
}
- (NSString *)lock1 {
NSString *lock1 = nil;
pthread_rwlock_rdlock(&_lock);
lock1 = [_lock1 copy]; //copy到新的地址,避免解锁后拿到旧值
pthread_rwlock_unlock(&_lock);
return lock1;
}
GCD的barrier读写锁
GCD的barrier栅栏功能相信大家都听说过,即在一个新创建的队列中,barrier功能可以保证,在他之前的异步队列执行完毕才指定barrier中间的内容,且还能保证barrier执行完毕后,才之后barrier之后的任务,且一个队列可以有多个barrier
因此此特性可以用于完成一个读写锁功能,即 barrier的代码块作为 写入操作模块
如下代码所示,由于需要引入 新创建队列,虽然使用起来不是不如pthread优秀,但这种思想却可以再恰当的时候发芽出新树苗
- (void)setupGCDRW {
_queue = dispatch_queue_create("RWLockQueue", DISPATCH_QUEUE_CONCURRENT);
}
#pragma mark --通过GCD的barrier栅栏功能实现
//通过GCD的barrier栅栏功能实现,缺点是需要借助自定义队列实现,且get方法无法重写系统的,只能以回调的方式获取值
//barrier功能使用global队列会失效,全局队列是无法阻塞的,里面有系统的一些任务执行
- (void)setLock2:(NSString *)lock2 {
dispatch_barrier_async(_queue, ^{
self->_lock2 = lock2;
});
}
- (void)getLock2WithBlock:(void(^)(NSString *))block {
dispatch_async(_queue, ^{
block(self->_lock2);
});
}
最后
相信看了这篇文章能给大家带来更多收货
最后,你能根据读写锁的特性,利用现有的锁,再写出一个完整的读写锁功能出来么!