美文网首页将来跳槽用iOS面试
iOS开发常用的几种锁!

iOS开发常用的几种锁!

作者: iOS鑫 | 来源:发表于2021-08-03 16:38 被阅读0次

    前言

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

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

    案例demo

    常见的多线程锁

    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互斥锁

    首先作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

    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);
        });
    }
    

    最后

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

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

    相关文章

      网友评论

        本文标题:iOS开发常用的几种锁!

        本文链接:https://www.haomeiwen.com/subject/lksevltx.html