ios常用的几种锁

2,401 阅读10分钟

前言

开发中引入了异步和多线程的来提高程序性能,也就意味着线程安全成为了多线程的一个障碍,因此线程锁应运而生,而锁如果用不好,还会造成死锁的风险

下面就介绍ios中常用的几种锁,以及读写锁的实现

案例demo

常见的多线程锁

ios中常见的几种锁包括OSSpinLock、信号量(Semaphore)、pthread_mutex、NSLock、NSCondition、NSConditionLock、pthread_mutex(recursive)、NSRecursiveLock、synchronized

如下所示,为前辈们测试锁性能的案例图(实际可能会略有偏差):

1899027-eb3ef0d444034362.webp

由于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协议,也属于互斥锁

并且再其基础上,新增了信号量功能 waitsignal,即 等待 和 释放 ,使用方式和 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);
    });
}

最后

相信看了这篇文章能给大家带来更多收货

最后,你能根据读写锁的特性,利用现有的锁,再写出一个完整的读写锁功能出来么!