美文网首页将来跳槽用iOS技术点iOS 好文
iOS多线程-各种线程锁的简单介绍

iOS多线程-各种线程锁的简单介绍

作者: Yasin的简书 | 来源:发表于2016-03-04 16:51 被阅读24115次

    demo下载

    建议一边看文章,一边看代码。

    声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果有所不同,所以性能分析只作为参考,仅代表测试代码表现,不代表真实情况。同时我会基于我的代码尽量让性能测试更精准。

    线程安全是怎么产生的

    常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。

    - (void)getIamgeName:(NSMutableArray *)imageNames{//假如每个进来的都是一个线程
        /*1.imageNames是线程外的变量,这个时候就需要考虑线程安全,
        因为,假如我们当前imageNames的个数是1,线程A和B同时进来发现个数是大于0的,
        都会去执行remove操作,结果肯定会有一个线程崩溃掉。
        */
        /*2.NSMutableArray *array = [[NSMutableArray alloc]initWithArray:imageNames];
        这里如果新生成一个array,下面也把imageNames换成array就不需要考虑线程安全,
        但是这样array.count判断永远大于0,也就是永远等于imageNames.count
         */
        NSString *imageName;
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObject:imageName];
        }
    }
    

    下面是锁的同步方案

    锁的概念

    锁是最常用的同步工具。一段代码段在同一个时间只能允许被一个线程访问,比如一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码。
    不要将过多的其他操作代码放到里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了。

    NSLock

    在Cocoa程序中NSLock中实现了一个简单的互斥锁,实现了NSLocking protocol。
    lock,加锁
    unlock,解锁
    tryLock,尝试加锁,如果失败了,并不会阻塞线程,只是立即返回
    NOlockBeforeDate:,在指定的date之前暂时阻塞线程(如果没有获取锁的话),如果到期还没有获取锁,则线程被唤醒,函数立即返回NO
    使用tryLock并不能成功加锁,如果获取锁失败就不会执行加锁代码了。

    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        [lock lock];
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObject:imageName];
        }
        [lock unlock];
    }
    

    @synchronized代码块

    每个iOS开发最早接触的线程锁就是@synchronized,代码简单。

    - (void)getIamgeName:(int)index{
        NSString *imageName;
        @synchronized(self) {
            if (imageNames.count>0) {
                imageName = [imageNames lastObject];
                [imageNames removeObject:imageName];
            }
        }
    }
    

    条件信号量dispatch_semaphore_t

    dispatch_semaphore_tGCD中信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量+1;每当发送一个等待信号时信号量-1,;如果信号量为0则信号会处于等待状态,直到信号量大于0开始执行。

    #import "MYDispatchSemaphoreViewController.h"
    
    @interface MYDispatchSemaphoreViewController ()
    {
        dispatch_semaphore_t semaphore;
    }
    @end
    
    @implementation MYDispatchSemaphoreViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        semaphore = dispatch_semaphore_create(1);
        /**
         *  创建一个信号量为1的信号
         *
         */
    }
    
    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        /**
         *  semaphore:等待信号
         DISPATCH_TIME_FOREVER:等待时间
         wait之后信号量-1,为0
         */
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObject:imageName];
        }
        /**
         *  发送一个信号通知,这时候信号量+1,为1
         */
        dispatch_semaphore_signal(semaphore);
    }
    
    @end
    

    条件锁NSCondition

    NSCondition同样实现了NSLocking协议,所以它和NSLock一样,也有NSLocking协议的lock和unlock方法,可以当做NSLock来使用解决线程同步问题,用法完全一样。

    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        [lock lock];
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObject:imageName];
        }
        [lock unlock];
    }
    

    同时,NSCondition提供更高级的用法。wait和signal,和条件信号量类似。
    比如我们要监听imageNames数组的个数,当imageNames的个数大于0的时候就执行清空操作。思路是这样的,当imageNames个数大于0时执行清空操作,否则,wait等待执行清空操作。当imageNames个数增加的时候发生signal信号,让等待的线程唤醒继续执行。
    NSCondition和NSLock、@synchronized等是不同的是,NSCondition可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。这是非常强大。
    但是正是因为这种分别加锁的方式,NSCondition使用wait并使用加锁后并不能真正的解决资源的竞争。比如我们有个需求:不能让m<0。假设当前m=0,线程A要判断到m>0为假,执行等待;线程B执行了m=1操作,并唤醒线程A执行m-1操作的同时线程C判断到m>0,因为他们在不同的线程锁里面,同样判断为真也执行了m-1,这个时候线程A和线程C都会执行m-1,但是m=1,结果就会造成m=-1.
    当我用数组做删除试验时,做增删操作并不是每次都会出现,大概3-4次后会出现。单纯的使用lock、unlock是没有问题的。

    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        [lock lock];    //加锁
        static int m = 0;
        static int n = 0;
        static int p = 0;
        NSLog(@"removeObjectBegin count: %ld\n",imageNames.count);
        
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObjectAtIndex:0];
            m++;
            NSLog(@"执行了%d次删除操作",m);
        } else {
            p++;
            NSLog(@"执行了%d次等待",p);
            [lock wait];    //等待
            imageName = [imageNames lastObject];
            [imageNames removeObjectAtIndex:0];
            /**
             *  有时候点击取出图片会崩溃
             */
            n++;
            NSLog(@"执行了%d次继续操作",n);
        }
        
        NSLog(@"removeObject count: %ld\n",imageNames.count);
        [lock unlock];     //解锁
    }
    - (void)createImageName:(NSMutableArray *)imageNames{
        [lock lock];
        static int m = 0;
        [imageNames addObject:@"0"];
        m++;
        NSLog(@"添加了%d次",m);
        [lock signal];  //唤醒随机一个线程取消等待继续执行
        
    //        [lock broadcast];   //唤醒所有线程取消等待继续执行
        NSLog(@"createImageName count: %ld\n",imageNames.count);
        [lock unlock];
    }
    
    #pragma mark - 多线程取出图片后删除
    - (void)getImageNameWithMultiThread{
        [lock broadcast];
        NSMutableArray *imageNames = [[NSMutableArray alloc]init];
        dispatch_group_t dispatchGroup = dispatch_group_create();
        __block double then, now;
        then = CFAbsoluteTimeGetCurrent();
        for (int i=0; i<10; i++) {
            dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
                [self getIamgeName:imageNames];
            });
            dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
                [self createImageName:imageNames];
            });
        }
        dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
            now = CFAbsoluteTimeGetCurrent();
            printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
        });
        
    }
    

    条件锁NSConditionLock

    也有人说这是个互斥锁
    NSConditionLock同样实现了NSLocking协议,试验过程中发现性能很低。

    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        [lock lock];
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObject:imageName];
        }
        [lock unlock];
    }
    

    NSConditionLock也可以像NSCondition一样做多线程之间的任务等待调用,而且是线程安全的。

    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        [lock lockWhenCondition:1];    //加锁
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObjectAtIndex:0];
        }
        [lock unlockWithCondition:0];     //解锁
    }
    - (void)createImageName:(NSMutableArray *)imageNames{
        [lock lockWhenCondition:0];
        [imageNames addObject:@"0"];
        [lock unlockWithCondition:1];
    }
    
    #pragma mark - 多线程取出图片后删除
    - (void)getImageNameWithMultiThread{
        NSMutableArray *imageNames = [[NSMutableArray alloc]init];
        dispatch_group_t dispatchGroup = dispatch_group_create();
        __block double then, now;
        then = CFAbsoluteTimeGetCurrent();
        for (int i=0; i<10000; i++) {
            dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
                [self getIamgeName:imageNames];
            });
            dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
                [self createImageName:imageNames];
            });
        }
        dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
            now = CFAbsoluteTimeGetCurrent();
            printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
        });
    }
    

    递归锁NSRecursiveLock

    有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。

    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        [lock lock];
        if (imageNames.count>0) {
            imageName = [imageNames firstObject];
            [imageNames removeObjectAtIndex:0];
            [self getIamgeName:imageNames];
        }
        [lock unlock];
    }
    - (void)getImageNameWithMultiThread{
        NSMutableArray *imageNames = [NSMutableArray new];
        int count = 1024*10;
        for (int i=0; i<count; i++) {
            [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
        }
        dispatch_group_t dispatchGroup = dispatch_group_create();
        __block double then, now;
        then = CFAbsoluteTimeGetCurrent();
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self getIamgeName:imageNames];
        });
        dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
            now = CFAbsoluteTimeGetCurrent();
            printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
        });
        
    }
    

    NSDistributedLock

    NSDistributedLock是MAC开发中的跨进程的分布式锁,底层是用文件系统实现的互斥锁。NSDistributedLock没有实现NSLocking协议,所以没有lock方法,取而代之的是非阻塞的tryLock方法。

    NSDistributedLock *lock = [[NSDistributedLock alloc] initWithPath:@"/Users/mac/Desktop/lock.lock"];
        while (![lock tryLock])
        {
            sleep(1);
        }
        
        //do something
        [lock unlock];
    

    当执行到do something时程序退出,程序再次启动之后tryLock就再也不能成功了,陷入死锁状态.其他应用也不能访问受保护的共享资源。在这种情况下,你可以使用breadLock方法来打破现存的锁以便你可以获取它。但是通常应该避免打破锁,除非你确定拥有进程已经死亡并不可能再释放该锁。
    因为是MAC下的线程锁,所以demo里面没有,这里也不做过多关注。

    互斥锁POSIX

    POSIX和dispatch_semaphore_t很像,但是完全不同。POSIX是Unix/Linux平台上提供的一套条件互斥锁的API。
    新建一个简单的POSIX互斥锁,引入头文件#import <pthread.h>声明并初始化一个pthread_mutex_t的结构。使用pthread_mutex_lock和pthread_mutex_unlock函数。调用pthread_mutex_destroy来释放该锁的数据结构。

    #import <pthread.h>
    @interface MYPOSIXViewController ()
    {
        pthread_mutex_t mutex;  //声明pthread_mutex_t的结构
    }
    @end
    
    @implementation MYPOSIXViewController
    - (void)dealloc{
        pthread_mutex_destroy(&mutex);  //释放该锁的数据结构
    }
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        pthread_mutex_init(&mutex, NULL);
        /**
         *  初始化
         *
         */
    }
    
    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        /**
         *  加锁
         */
        pthread_mutex_lock(&mutex);
        if (imageNames.count>0) {
            imageName = [imageNames firstObject];
            [imageNames removeObjectAtIndex:0];
        }
        /**
         *  解锁
         */
        pthread_mutex_unlock(&mutex);
    }
    

    POSIX还可以创建条件锁,提供了和NSCondition一样的条件控制,初始化互斥锁同时使用pthread_cond_init来初始化条件数据结构,

        // 初始化
        int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
        
        // 等待(会阻塞)
        int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
        
        // 定时等待
        int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
        
        // 唤醒
        int pthread_cond_signal (pthread_cond_t *cond);
        
        // 广播唤醒
        int pthread_cond_broadcast (pthread_cond_t *cond);
        
        // 销毁
        int pthread_cond_destroy (pthread_cond_t *cond);
    

    POSIX还提供了很多函数,有一套完整的API,包含Pthreads线程的创建控制等等,非常底层,可以手动处理线程的各个状态的转换即管理生命周期,甚至可以实现一套自己的多线程,感兴趣的可以继续深入了解。推荐一篇详细文章,但不是基于iOS的,是基于Linux的,但是介绍的非常详细 Linux 线程锁详解

    自旋锁OSSpinLock

    首先要提的是OSSpinLock已经出现了BUG,导致并不能完全保证是线程安全的。

    新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
    具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。
    苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。
    OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
    -摘自ibireme

    所以说不建议再继续使用,不过可以拿来玩耍一下,导入头文件#import <libkern/OSAtomic.h>

    #import <libkern/OSAtomic.h>
    @interface MYOSSpinLockViewController ()
    {
        OSSpinLock spinlock;  //声明pthread_mutex_t的结构
    }
    @end
    
    @implementation MYOSSpinLockViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        spinlock = OS_SPINLOCK_INIT;
        /**
         *  初始化
         *
         */
    }
    
    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        /**
         *  加锁
         */
        OSSpinLockLock(&spinlock);
        if (imageNames.count>0) {
            imageName = [imageNames firstObject];
            [imageNames removeObjectAtIndex:0];
        }
        /**
         *  解锁
         */
        OSSpinLockUnlock(&spinlock);
    }
    @end
    

    OSSpinLock的性能真的很卓越,可惜啦

    GCD线程阻断dispatch_barrier_async/dispatch_barrier_sync

    dispatch_barrier_async/dispatch_barrier_sync在一定的基础上也可以做线程同步,会在线程队列中打断其他线程执行当前任务,也就是说只有用在并发的线程队列中才会有效,因为串行队列本来就是一个一个的执行的,你打断执行一个和插入一个是一样的效果。两个的区别是是否等待任务执行完成。

    注意:如果在当前线程调用dispatch_barrier_sync打断会发生死锁。

    @interface MYdispatch_barrier_syncViewController ()
    {
            __block double then, now;
    }
    @end
    
    @implementation MYdispatch_barrier_syncViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
    }
    - (void)getIamgeName:(NSMutableArray *)imageNames{
        NSString *imageName;
        if (imageNames.count>0) {
            imageName = [imageNames firstObject];
            [imageNames removeObjectAtIndex:0];
        }else{
            now = CFAbsoluteTimeGetCurrent();
            printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
        }
    }
    
    - (void)getImageNameWithMultiThread{
        NSMutableArray *imageNames = [NSMutableArray new];
        int count = 1024*11;
        for (int i=0; i<count; i++) {
            [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
        }
        then = CFAbsoluteTimeGetCurrent();
        for (int i=0; i<count+1; i++) {
            //100来测试锁有没有正确的执行
            dispatch_barrier_async(self.synchronizationQueue, ^{
                 [self getIamgeName:imageNames];
            });
        }
    }
    
    

    总结

    @synchronized:适用线程不多,任务量不大的多线程加锁
    NSLock:其实NSLock并没有想象中的那么差,不知道大家为什么不推荐使用
    dispatch_semaphore_t:使用信号来做加锁,性能提升显著
    NSCondition:使用其做多线程之间的通信调用不是线程安全的
    NSConditionLock:单纯加锁性能非常低,比NSLock低很多,但是可以用来做多线程处理不同任务的通信调用
    NSRecursiveLock:递归锁的性能出奇的高,但是只能作为递归使用,所以限制了使用场景
    NSDistributedLock:因为是MAC开发的,就不讨论了
    POSIX(pthread_mutex):底层的api,复杂的多线程处理建议使用,并且可以封装自己的多线程
    OSSpinLock:性能也非常高,可惜出现了线程问题
    dispatch_barrier_async/dispatch_barrier_sync:测试中发现dispatch_barrier_sync比dispatch_barrier_async性能要高,真是大出意外

    下面是基准测试
    模拟器环境:i5 2.6GH+8G 内存,xcode 7.2.1 (7C1002)+iPhone6SP(9.2)

    多线程锁删除数组性能测试(模拟器).png

    真机环境:xcode 7.2.1 (7C1002)+iPhone6(国行)

    多线程锁删除数组性能测试(iPhone6真机).png

    通过测试发现模拟器和真机的区别还是很大的,模拟器上明显的阶梯感,真机就没有,模拟器上NSConditionLock的性能非常差,我没有把它的参数加在表格上,不然其他的就看不到了。不过真机上面性能还好。

    这些性能测试只是一个参考,没必要非要去在意这些,毕竟前端的编程一般线程要求没那么高,可以从其他的地方优化。线程安全中注意避坑,另外选择自己喜欢的方式,这样你可以研究的更深入,使用的更熟练。

    另外,demo中我把逻辑拿了出来,算是一个小小的MVVM框架或者MVVCC框架吧
    demo在最上方。

    2016.6.30更新

    有网友提醒我有些锁在资源竞争激烈和不激烈的情况下性能有差别,于是我修改了源码,将原来的开辟大量线程逻辑改为开辟3个线程,代码已更新github,老代码在标签1.0的位置,有兴趣可以看下。

    多线程锁删除数组性能测试(iPhone6真机)2.png

    根据新的测试结果,dispatch_barrier_async、dispatch_semaphore_t 、OSSpinLock,三种锁在资源竞争程度不同下表现比较明显。

    另外再次声明:测试结果仅仅代表一个参考,因为各种因素的影响,并没有那么准确。还是那句话,选择自己喜欢的加锁方式,高大上的还是性能高的,自己选择,没必要太在意对比。

    相关文章

      网友评论

      • 峰云逸飞:加锁之前那段,你说会崩溃,我不以为然,其实不会崩溃。博主能给个理由吗?为什么可变数组可以remove多次,虽然是并发执行,但是arr的地址是一样的
        Yasin的简书:这里主要是测试锁的性能的,崩溃测试的意义不大
        Yasin的简书:和arr的地址无关,和越界有关
        Yasin的简书:removeObject确实不会崩溃,但是如果是removeObjectAtIndex的话就会
      • LikeSomeBody:DEMO 中 的注释
        //没过5秒进行一项测试,防止CPU占用冲突
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3*idx * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        id class = NSClassFromString([NSString

        实际 上是 3秒 不知是否 正确?
      • WHZ闹哪样:请问线程锁的应用场景是什么,没有想到什么场景下会有多个线程同时操作线程外的对象的场景啊
        Yasin的简书:@WHZ闹哪样 AF中有个tasksForKeyPath方法,你可以去看看,理论上只要你创建了异步线程去操作一个对象了,那你就需要给这个对象加锁,除非你非常确定你的这个对象绝对不可能同时被操作,一般情况下你都不能保证的
      • Lision:受教了
      • 童话镇里蜿蜒的河:楼主你好 GCD的异步线程需要用到线程锁吗? 是不是只有同步线程才会需要线程锁?
        Yasin的简书:@童话镇里蜿蜒的河 多线程操作同一个对象的时候就需要加锁。和同步异步没有关系,只要可能不在一个线程就需要,当然,我们大部分操作都是在主线程的
      • 酷走天涯:那个效率最高
      • 酷走天涯:不错写的很好,大神终于找到你了,😄
      • 5791b3d0479b:太详细
      • nlpjoe:请问下图是怎么做出来的?
        Yasin的简书:@周俊佐 excel
      • 6dfb13c70edf:写的真好,学习了
      • 齐刘海姑娘:哈喽~您的文章已收录专题【我不是程序猿,请叫我攻城狮】http://www.jianshu.com/collection/db91065b98c6,欢迎关注投稿哦~ :kissing_heart: :heart: :heart: :smile:
      • e2f2d779c022:不错,加油
      • 十字云:干货
      • Cyandev:mark
      • 笨鸟后飞了:得慢慢消化

      本文标题:iOS多线程-各种线程锁的简单介绍

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