美文网首页
iOS开发中的锁

iOS开发中的锁

作者: nickNameDC | 来源:发表于2019-05-06 16:05 被阅读0次

    iOS开发中的锁

    本人对锁没有深入理解,只是看了几篇文章,在这里做一下简单的总结。

    iOS开发中,锁是用来解决线程安全的问题的工具。那么线程安全是什么?

    线程安全


    线程安全:多线程操作共享数据的时候,如果出现了意想不到的结果,就是线程不安全。反之就是线程安全;

    或者这么说是不是更容易听懂,多个线程同时对一个数据进行修改的时候,如果不能保证多个线程的执行顺序,就会出现意想不到的结果,这个时候就线程不安全了。

    貌似怎么说都不行了,那么就举个例子吧;

    - (void)threadNotSafe {
       __block NSInteger total = 0;
        for (NSInteger index = 0; index < 3; index++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                total += 1;
                NSLog(@"total: %ld", total);
                total -= 1;
                NSLog(@"total: %ld", total);
            });
        }
    }
    /*
    第一次打印结果
    2019-05-06 11:04:33.937462+0800 DCLockStudy[5270:410073] total: 1
    2019-05-06 11:04:33.937462+0800 DCLockStudy[5270:410074] total: 2
    2019-05-06 11:04:33.937466+0800 DCLockStudy[5270:410075] total: 3
    2019-05-06 11:04:33.937617+0800 DCLockStudy[5270:410075] total: 2
    2019-05-06 11:04:33.937617+0800 DCLockStudy[5270:410073] total: 1
    2019-05-06 11:04:33.937617+0800 DCLockStudy[5270:410074] total: 2
    第二次打印结果
    2019-05-06 11:06:50.198993+0800 DCLockStudy[5320:416449] total: 1
    2019-05-06 11:06:50.198994+0800 DCLockStudy[5320:416450] total: 2
    2019-05-06 11:06:50.199020+0800 DCLockStudy[5320:416452] total: 3
    2019-05-06 11:06:50.199187+0800 DCLockStudy[5320:416450] total: 2
    2019-05-06 11:06:50.199187+0800 DCLockStudy[5320:416449] total: 1
    2019-05-06 11:06:50.199253+0800 DCLockStudy[5320:416452] total: 0
    */
    

    上面这段代码,分别执行两次,打印结果不一样。也就是不能确定代码执行顺序和执行结果,是线程不安全的;

    再看下面这段代码

    - (void)threadSafe {
        __block NSInteger total = 0;
        NSLock *myLock = [[NSLock alloc]init];
        for (NSInteger index = 0; index < 3; index++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [myLock lock];
                total += 1;
                NSLog(@"total: %ld", total);
                total -= 1;
                NSLog(@"total: %ld", total);
                [myLock unlock];
            });
        }
    }
    /*
    第一次打印结果
    2019-05-06 11:10:03.678707+0800 DCLockStudy[5351:422830] total: 1
    2019-05-06 11:10:03.678872+0800 DCLockStudy[5351:422830] total: 0
    2019-05-06 11:10:03.678978+0800 DCLockStudy[5351:422829] total: 1
    2019-05-06 11:10:03.679057+0800 DCLockStudy[5351:422829] total: 0
    2019-05-06 11:10:03.679189+0800 DCLockStudy[5351:422828] total: 1
    2019-05-06 11:10:03.679286+0800 DCLockStudy[5351:422828] total: 0
    第二次打印结果
    2019-05-06 11:14:52.524955+0800 DCLockStudy[5406:431979] total: 1
    2019-05-06 11:14:52.525092+0800 DCLockStudy[5406:431979] total: 0
    2019-05-06 11:14:52.525224+0800 DCLockStudy[5406:431980] total: 1
    2019-05-06 11:14:52.525303+0800 DCLockStudy[5406:431980] total: 0
    2019-05-06 11:14:52.525413+0800 DCLockStudy[5406:431978] total: 1
    2019-05-06 11:14:52.525511+0800 DCLockStudy[5406:431978] total: 0
    */
    

    两次打印结果一样,为什么呢?因为加了锁,哈哈哈;那么接下来我们来简单说一下锁;

    锁的几个的定义


    • 临界区:每个进程中访问临界资源的那段程序称为临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。

    • 互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

    接下来主要讲几种锁:自旋锁OSSpinLock、信号量、pthread_mutex、NSLock、NSCondition、NSRecursiveLock、NSConditionLock、@synchronized。这里参考了深入理解iOS开发中的锁

    然后这些锁我在学习过程中写了一个简单的demo,里面有他们的使用方法;

    自旋锁OSSpinLock


    自旋锁与互斥锁类似,它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。一般用于锁持有的时间短,而且线程并不希望在重新调度上花太多的成本。

    自旋锁与互斥锁的区别:线程在申请自旋锁的时候,线程不会被挂起,而是处于忙等的状态。

    我所知道的自旋锁只有OSSpinLock,不过YY大神已经说过OSSpinLock不再安全了,因此这里不做过多的介绍,如果有兴趣可以去看不再安全的 OSSpinLock;

    信号量


    dispatch_semaphore的实现原理和自旋锁不一样,是根据信号量判断的,首先会将信号量-1,并判断是否大于等于0,如果是,则返回0,并继续执行后续代码,否则,使线程进入睡眠状态,让出cpu时间。直到信号量大于0或者超时,则线程会被重新唤醒执行后续操作。

    使用方法如下

    - (void)__dispatch_semaphore{
        /**
            dispatch_semaphore_create(1): 传入值必须 >=0, 若传入为 0 则阻塞线程并等待timeout,时间到后会执行其后的语句
            dispatch_semaphore_wait(signal, overTime):可以理解为 lock,会使得 signal 值 -1
            dispatch_semaphore_signal(signal):可以理解为 unlock,会使得 signal 值 +1
        
            停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
            信号量的值(signal)就相当于剩余车位的数目,dispatch_semaphore_wait 函数就相当于来了一辆车,dispatch_semaphore_signal 就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),调用一次 dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait 剩余车位就减少一个;当剩余车位为 0 时,再来车(即调用 dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。
        */
        dispatch_semaphore_t signal = dispatch_semaphore_create(1);//传入值必须>=0;如果传入0,
        dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW,2.0f * NSEC_PER_SEC);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"线程1等待中。。。");
            dispatch_semaphore_wait(signal, overTime);
            NSLog(@"线程1");
            sleep(1);
            dispatch_semaphore_signal(signal);
            NSLog(@"线程1发送信号");
        });
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"线程2等待中。。。");
            dispatch_semaphore_wait(signal, overTime);
            NSLog(@"线程2");
            sleep(1);
            dispatch_semaphore_signal(signal);
            NSLog(@"线程2发送信号");
        });
    }
    

    pthread_mutex


    pthread_mutex 表示互斥锁。互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。

    使用方法如下:

    - (void)__pthread_mutex_t{
        /**
         声明 pthread_mutex_t pMutex;
            创建一个互斥锁pthread_mutex_init(&pMutex,PTHREAD_MUTEX_NORMAL);
             PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
             PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
             PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
             PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
            加锁 pthread_mutex_lock(&pMutex);
            解锁 pthread_mutex_unlock(&pMutex);
     */
        //线程1
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"线程1加锁");
            pthread_mutex_lock(&pMutex);
            sleep(1);
            NSLog(@"线程1");
            pthread_mutex_unlock(&pMutex);
            NSLog(@"线程1解锁");
        });
        //线程2
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(1); //保证线程1先加锁
            NSLog(@"线程2加锁");
            pthread_mutex_lock(&pMutex);
            NSLog(@"线程2");
            pthread_mutex_unlock(&pMutex);
            NSLog(@"线程2解锁");
        });
    }
    

    pthread_mutex 还支持递归锁,只要将类型设置为PTHREAD_MUTEX_RECURSIVE就可以。

    NSLock


    NSLock是OC以对象的形式暴露给开发者的一种锁,其实NSLock只是在内部封装了一个pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。

    使用方法如下:

    NSLock *lock = [NSLock new];
    [lock lock];
    //需要执行的代码
    [lock unlock];
    

    NSLock遵守了NSLocking协议,NSLocking协议其实很简单,只需要满足两个方法

    @protocol NSLocking
    - (void)lock;
    - (void)unlock;
    @end
    

    在这个基础上,NSLock还自己提供两个方法

    - (BOOL)tryLock;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    • tryLock 尝试获取锁,如果这个时候,别的线程添加了锁,则返回NO,不会阻塞线程;

    • lockBeforeDate尝试在某个时间之前获取锁,如果在这个时间内没有获取到锁则返回NO,不会阻塞线程;

    NSCondition


    NSCondition其实是通过封装了一个互斥锁和条件变量,把互斥锁的lock方法和条件变量的wait/signal统一在NSCondition对象中,暴露给使用者。

    NSCondition的加锁过程和NSLock几乎一致,耗时上应该差不多。

    使用方法如下:

    - (void)__NSCondition{
        /** 条件变量
         wait:进入等待状态
         waitUntilDate::让一个线程等待一定的时间
         signal:唤醒一个等待的线程
         broadcast:唤醒所有等待的线程
         */
        //线程1
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self.condition lock];
            NSLog(@"线程1获取到锁,并进入等待状态");
            [self.condition wait];
            NSLog(@"线程1等待完成");
            [self.condition unlock];
            NSLog(@"线程1解锁");
        });
        
        //线程2
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self.condition lock];
            NSLog(@"线程2获取到锁,并进入等待状态");
            [self.condition wait];
            NSLog(@"线程2等待完成");
            [self.condition unlock];
            NSLog(@"线程2解锁");
        });
        
        //线程3
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(1);
            NSLog(@"线程3,唤醒一个等待线程");
            [self.condition signal];
            
            sleep(2);
            NSLog(@"线程3,唤醒所有等待线程");
            [self.condition broadcast];
        });
    }
    
    

    NSConditionLock


    NSConditionLock 借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性进行赋值。

    使用方法如下:

    - (void)__NSConditionLock{
        /**条件锁  NSConditionLock
            因为遵守了NSLocking协议,所以可以无条件加锁lock,
         */
        dispatch_queue_t conditionLockQueue = dispatch_queue_create("conditionLockQueue", DISPATCH_QUEUE_CONCURRENT);
        //线程1
        dispatch_async(conditionLockQueue, ^{
            NSLog(@"进入线程1,添加条件锁 = 2,如果condition != 2,则线程阻塞");
            [self.conditionLock lockWhenCondition:2];
            NSLog(@"线程1加锁成功,lockWhenCondition:2");
            [self.conditionLock unlock];
            NSLog(@"线程1解锁锁成功,unlockWithCondition:1");
        });
        
        //线程2
        dispatch_async(conditionLockQueue, ^{
            sleep(1); //保证线程3先执行
            NSLog(@"进入线程2,尝试添加条件锁 = 1");
            if([self.conditionLock tryLockWhenCondition:1]){
                NSLog(@"线程2加锁成功,lockWhenCondition:1");
                [self.conditionLock unlockWithCondition:2];
                NSLog(@"线程2解锁成功,unlockWithCondition:1");
            }else{
                NSLog(@"线程2加锁失败");
            }
        });
        
        //线程3
        dispatch_async(conditionLockQueue, ^{
    //        sleep(1); //保证线程2先执行
            NSLog(@"进入线程3,尝试添加条件锁 = 0");
            if([self.conditionLock tryLockWhenCondition:0]){
                NSLog(@"线程3加锁成功,lockWhenCondition:0");
                [self.conditionLock unlockWithCondition:1];
                NSLog(@"线程3解锁成功,unlockWithCondition:1");
            }else{
                NSLog(@"线程3加锁失败");
            }
        });
        
        /** 先进入线程1,条件不满足,线程1阻塞,
            进入线程3,线程3满足条件,线程3加锁,线程3解锁并将条件设置为1;
            进入线程2,线程满足条件,线程2加锁,线程2解锁,并将条件设置为2;
            因为条件为2,线程1满足条件了,线程1不在阻塞,线程1加锁,线程1解锁;
         */
    }
    

    NSRecursiveLock


    递归锁是通过pthread_mutex_lock函数来实现的。

    NSRecursiveLockNSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE

    使用方法如下:

    - (void)__NSRecursiveLock{
        /** NSRecursiveLock递归锁
            它可以被同一线程多次请求,但不会引起死锁。这主要是用在循环或者递归操作场景中。
         */
        /** 如果用普通锁,当第二次进入递归方法时,尝试加锁,但是这个时候该线程还处于锁定,所以线程阻塞,相互等待造成死锁。
         所以这个时候可以用一个递归锁,允许同一线程多次请求,并且不会死锁;
         NSRecursiveLock 递归锁和NSLock一样也有tryLock和lockBeforeDate,用法一样;
         */
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            static void (^ recursiveMethod)(NSInteger);
            recursiveMethod = ^(NSInteger num){
                //    [self.myLock lock];
                [self.recursiveLock lock];
                if(num > 0){
                    num --;
                    NSLog(@"start num = %ld, mutableArray[0] = %@",num,self.mutableArray[0]);
                    sleep(1);
                    [self.mutableArray replaceObjectAtIndex:0 withObject:[NSString stringWithFormat:@"replace = %ld",num]];
                    NSLog(@"end num = %ld, mutableArray[0] = %@",num,self.mutableArray[0]);
                    recursiveMethod(num);
                }
                //    [self.myLock unlock];
                [self.recursiveLock unlock];
            };
            recursiveMethod(5);
        });
    }
    

    @synchronized


    这其实是一个OC层面的锁,主要通过牺牲新能来换取语法上的简洁与可读。

    我们知道 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值来得到对应的互斥锁。

    具体实现原理可以参考 关于 @synchronized,这儿比你想知道的还要多

    [demo]

    这是我自己学习的时候写的一个demo,是各种锁的使用。上面的代码基本都是这个demo的代码;

    参考资料


    1、深入理解iOS开发中的锁

    2、iOS的线程安全与锁

    相关文章

      网友评论

          本文标题:iOS开发中的锁

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