美文网首页收藏ios
关于iOS中的13种加锁方案(下)

关于iOS中的13种加锁方案(下)

作者: 一意孤行的程序猿 | 来源:发表于2019-12-11 22:53 被阅读0次

    前言

    iOS中有很多锁,那么平时使用过程中到底怎么使用呢?本文分享13种加锁方案。本文较长总共一万字。文中代码在github上。

    • OSSpinLock自旋锁
    • os_unfair_lock互斥锁
    • pthread_mutex递归锁
    • pthread_mutex条件锁
    • dispatch_semaphore信号量
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
    • NSLock
    • NSRecursiveLock
    • NSCondition
    • NSConditionLock
    • @synchronized
    • dispatch_barrier_async栅栏
    • dispatch_group调度组

    性能对比:借用ibireme大神的一张图片

    信号量原理

    dispatch_semaphore_create(long value); // 创建信号量
    dispatch_semaphore_signal(dispatch_semaphore_t deem); // 发送信号量
    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信号量
    

    dispatch_semaphore_create(long value)和GCD的group等用法一致,这个函数是创建一个dispatch_semaphore_类型的信号量,并且创建的时候需要指定信号量的大小。

    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) 等待信号量。如果信号量值为0,那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。

    dispatch_semaphore_signal(dispatch_semaphore_t deem) 发送信号量。该函数会对信号量的值进行加1操作。

    通常等待信号量和发送信号量的函数是成对出现的。并发执行任务时候,在当前任务执行之前,用dispatch_semaphore_wait函数进行等待(阻塞),直到上一个任务执行完毕后且通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),dispatch_semaphore_wait函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1,然后当前任务可以执行,执行完毕当前任务后,再通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),通知执行下一个任务......如此一来,通过信号量,就达到了并发队列中的任务同步执行的要求。

    使用

    先看加锁,解锁的使用,初始化先设置1,然后每次取钱,存钱之前,都调用dispatch_semaphore_wait,取钱,存钱之后调用dispatch_semaphore_signal

    eg:

    YZSemaphore继承YZBaseLockotherTest里面进行测试

    #import "YZSemaphore.h"
    
    @interface YZSemaphore()
    @property (strong, nonatomic) dispatch_semaphore_t moneySemaphore;
    @end
    
    @implementation YZSemaphore
    - (instancetype)init
    {
        if (self = [super init]) {
            self.moneySemaphore = dispatch_semaphore_create(1);
        }
        return self;
    }
    
    - (void)__drawMoney
    {
        dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
        [super __drawMoney];
    
        dispatch_semaphore_signal(self.moneySemaphore);
    }
    
    - (void)__saveMoney
    {
        dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
        [super __saveMoney];
    
        dispatch_semaphore_signal(self.moneySemaphore);
    }
    

    外部调用的时候,

    YZBaseLock *lock = [[YZSemaphore alloc] init];
    [lock moneyTest];
    

    输出

    iOS-LockDemo[13500:171371] 存10元,还剩110元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
    iOS-LockDemo[13500:171369] 取20元,还剩90元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
    iOS-LockDemo[13500:171371] 存10元,还剩100元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
    iOS-LockDemo[13500:171369] 取20元,还剩80元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
    iOS-LockDemo[13500:171371] 存10元,还剩90元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
    iOS-LockDemo[13500:171369] 取20元,还剩70元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
    iOS-LockDemo[13500:171371] 存10元,还剩80元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
    iOS-LockDemo[13500:171369] 取20元,还剩60元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
    iOS-LockDemo[13500:171371] 存10元,还剩70元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
    iOS-LockDemo[13500:171369] 取20元,还剩50元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
    

    有结果可知,能保证多线程数据的安全读写。

    使用二

    信号量还可以控制线程数量,例如初始化的时候,设置最多3条线程

    #import "YZSemaphore.h"
    
    @interface YZSemaphore()
    @property (strong, nonatomic) dispatch_semaphore_t semaphore;
    @end
    
    @implementation YZSemaphore
    - (instancetype)init
    {
        if (self = [super init]) {
            self.semaphore = dispatch_semaphore_create(3);
        }
        return self;
    }
    
    - (void)otherTest
    {
        for (int i = 0; i < 20; i++) {
            [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
        }
    }
    
    // 线程10、7、6、9、8
    - (void)test
    {
        // 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
        // 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
        sleep(2);
        NSLog(@"test - %@", [NSThread currentThread]);
    
        // 让信号量的值+1
        dispatch_semaphore_signal(self.semaphore);
    }
    
    @end
    

    调用otherTest的输出结果为

    2018-08-14 16:38:56.489121+0800 iOS-LockDemo[14002:180654] test - <NSThread: 0x600003a938c0>{number = 3, name = (null)}
    2018-08-14 16:38:56.492100+0800 iOS-LockDemo[14002:180655] test - <NSThread: 0x600003a93900>{number = 4, name = (null)}
    2018-08-14 16:38:56.492281+0800 iOS-LockDemo[14002:180656] test - <NSThread: 0x600003a93940>{number = 5, name = (null)}
    
    2018-08-14 16:38:58.497578+0800 iOS-LockDemo[14002:180657] test - <NSThread: 0x600003a93980>{number = 6, name = (null)}
    2018-08-14 16:38:58.499225+0800 iOS-LockDemo[14002:180658] test - <NSThread: 0x600003a8e640>{number = 7, name = (null)}
    2018-08-14 16:38:58.549633+0800 iOS-LockDemo[14002:180659] test - <NSThread: 0x600003a93a00>{number = 8, name = (null)}
    
    2018-08-14 16:39:00.499672+0800 iOS-LockDemo[14002:180660] test - <NSThread: 0x600003aa6cc0>{number = 9, name = (null)}
    2018-08-14 16:39:00.499799+0800 iOS-LockDemo[14002:180661] test - <NSThread: 0x600003aa6ec0>{number = 10, name = (null)}
    2018-08-14 16:39:00.550353+0800 iOS-LockDemo[14002:180662] test - <NSThread: 0x600003aa6d80>{number = 11, name = (null)}
    
    2018-08-14 16:39:02.501379+0800 iOS-LockDemo[14002:180663] test - <NSThread: 0x600003aa6f00>{number = 12, name = (null)}
    

    由结果可知,每次最多三条线程执行。

    synchronized

    • @synchronized是对mutex递归锁的封装
    • 源码查看:objc4中的objc-sync.mm文件
    • @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

    详细了解可以参考 关于 @synchronized,这儿比你想知道的还要多

    使用

    @synchronized 使用起来很简单

    还使用前面的存钱取票的例子,类YZSynchronized继承自YZBaseLock,代码如下

    #import "YZSynchronized.h"
    
    @interface YZSynchronized()
    
    @end
    
    @implementation YZSynchronized
    - (void)__saveMoney
    {
        @synchronized (self) {
            [super __saveMoney];
        }
    }
    
    - (void)__drawMoney
    {
        @synchronized (self) {
            [super __drawMoney];
        }
    }
    @end
    

    调用之后

    iOS-LockDemo[2573:45093] 存10元,还剩110元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
    iOS-LockDemo[2573:45093] 存10元,还剩120元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
    iOS-LockDemo[2573:45093] 存10元,还剩130元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
    iOS-LockDemo[2573:45095] 取20元,还剩110元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
    iOS-LockDemo[2573:45095] 取20元,还剩90元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
    iOS-LockDemo[2573:45095] 取20元,还剩70元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
    iOS-LockDemo[2573:45095] 取20元,还剩50元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
    iOS-LockDemo[2573:45095] 取20元,还剩30元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
    iOS-LockDemo[2573:45093] 存10元,还剩40元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
    iOS-LockDemo[2573:45093] 存10元,还剩50元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
    

    可知,多线程的数据没有发生错乱

    源码分析

    runtime源码中的objc-sync.mm中可知

    int objc_sync_enter(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
    
        if (obj) {
            SyncData* data = id2data(obj, ACQUIRE);
            assert(data);
            data->mutex.lock();
        } else {
            // @synchronized(nil) does nothing
            if (DebugNilSync) {
                _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
            }
            objc_sync_nil();
        }
    
        return result;
    }
    
    // End synchronizing on 'obj'. 
    // Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
    int objc_sync_exit(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
    
        if (obj) {
            SyncData* data = id2data(obj, RELEASE); 
            if (!data) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            } else {
                bool okay = data->mutex.tryUnlock();
                if (!okay) {
                    result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
                }
            }
        } else {
            // @synchronized(nil) does nothing
        }
    
        return result;
    }
    
    typedef struct alignas(CacheLineSize) SyncData {
        struct SyncData* nextData;
        DisguisedPtr<objc_object> object;
        int32_t threadCount;  // number of THREADS using this block
        recursive_mutex_t mutex;
    } SyncData;
    

    以及

    using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;
    

    可知@synchronized是对mutex递归锁的封装。因为是递归锁,可以递归加锁,读者有兴趣自行验证。

    pthread_rwlock读写锁

    读写锁是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁。多读者锁,“push lock”) 用于解决读写问题。读操作可并发重入,写操作是互斥的。

    • 需要导入头文件#import <pthread.h>

    使用

    // 初始化锁
    pthread_rwlock_t lock;
    pthread_rwlock_init(&lock, NULL);
    // 读-加锁
    pthread_rwlock_rdlock(&lock);
    // 读-尝试加锁
    pthread_rwlock_tryrdlock(&lock);
    // 写-加锁
    pthread_rwlock_wrlock(&lock);
    // 写-尝试加锁
    pthread_rwlock_trywrlock(&lock);
    // 解锁
    pthread_rwlock_unlock(&lock);
    // 销毁
    pthread_rwlock_destroy(&lock);
    

    eg:

    YZRwlock继承YZBaseLockotherTest里面进行测试,每次读,或者写的之前进行加锁,并sleep 1秒钟,之后解锁,如下所示

    #import "YZRwlock.h"
    #import <pthread.h>
    
    @interface YZRwlock()
    @property (assign, nonatomic) pthread_rwlock_t lock;
    
    @end
    
    @implementation YZRwlock
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            // 初始化锁
            pthread_rwlock_init(&_lock, NULL);
        }
        return self;
    }
    
    - (void)otherTest{
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
        for (int i = 0; i < 3; i++) {
            dispatch_async(queue, ^{
                [self write];
                [self read];
            });
    
        }
        for (int i = 0; i < 3; i++) {
            dispatch_async(queue, ^{
                [self write];
            });
        }
    }
    
    - (void)read {
        pthread_rwlock_rdlock(&_lock);
    
        sleep(1);
        NSLog(@"%s", __func__);
    
        pthread_rwlock_unlock(&_lock);
    }
    
    - (void)write
    {
        pthread_rwlock_wrlock(&_lock);
    
        sleep(1);
        NSLog(@"%s", __func__);
    
        pthread_rwlock_unlock(&_lock);
    }
    
    - (void)dealloc
    {
        pthread_rwlock_destroy(&_lock);
    }
    
    @end
    

    调用的时候

    YZBaseLock *lock = [[YZRwlock alloc] init];
    [lock otherTest];
    

    输出结果为:

    2018-08-15 16:07:45.753659+0800 iOS-LockDemo[25457:248359] -[YZRwlock write]
    2018-08-15 16:07:46.758460+0800 iOS-LockDemo[25457:248356] -[YZRwlock write]
    2018-08-15 16:07:47.763705+0800 iOS-LockDemo[25457:248358] -[YZRwlock write]
    2018-08-15 16:07:48.767980+0800 iOS-LockDemo[25457:248381] -[YZRwlock write]
    2018-08-15 16:07:49.772241+0800 iOS-LockDemo[25457:248382] -[YZRwlock write]
    2018-08-15 16:07:50.777547+0800 iOS-LockDemo[25457:248383] -[YZRwlock write]
    2018-08-15 16:07:51.779544+0800 iOS-LockDemo[25457:248359] -[YZRwlock read]
    2018-08-15 16:07:51.779544+0800 iOS-LockDemo[25457:248356] -[YZRwlock read]
    2018-08-15 16:07:51.779546+0800 iOS-LockDemo[25457:248358] -[YZRwlock read]
    

    由结果可知,打印完write之后,方法每次都是一个一个执行的,而read是可以同时执行的,也就是说达到了多读单写的功能。被称为读写锁。

    dispatch_barrier_async异步栅栏

    • 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
    • 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果

    使用

    // 初始化队列
    self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    
    // 读
    dispatch_async(self.queue, ^{
    
    });
    
    // 写
    dispatch_barrier_async(self.queue, ^{
    
    });
    

    eg:

    YZBarrier继承YZBaseLockotherTest里面进行测试

    #import "YZBarrier.h"
    
    @interface YZBarrier ()
    @property (strong, nonatomic) dispatch_queue_t queue;
    @end
    @implementation YZBarrier
    
    - (void)otherTest{
    
        // 初始化队列
        self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    
        for (int i = 0; i < 3; i++) {
            // 读
            dispatch_async(self.queue, ^{
                [self read];
            });
             // 写
            dispatch_barrier_async(self.queue, ^{
                [self write];
            });
             // 读
            dispatch_async(self.queue, ^{
                [self read];
            });
             // 读
            dispatch_async(self.queue, ^{
                [self read];
            });
    
        }
    }
    
    - (void)read {
        sleep(1);
        NSLog(@"read");
    }
    
    - (void)write
    {
        sleep(1);
        NSLog(@"write");
    }
    @end
    

    调用的时候

    YZBaseLock *lock = [[YZBarrier alloc] init];
    [lock otherTest];
    

    输出结果为:

    2018-08-15 17:50:45.867990+0800 iOS-LockDemo[30046:324146] read
    2018-08-15 17:50:46.871969+0800 iOS-LockDemo[30046:324146] write
    2018-08-15 17:50:47.876419+0800 iOS-LockDemo[30046:324146] read
    2018-08-15 17:50:47.876419+0800 iOS-LockDemo[30046:324148] read
    2018-08-15 17:50:47.876450+0800 iOS-LockDemo[30046:324145] read
    2018-08-15 17:50:48.880739+0800 iOS-LockDemo[30046:324145] write
    2018-08-15 17:50:49.885434+0800 iOS-LockDemo[30046:324145] read
    2018-08-15 17:50:49.885435+0800 iOS-LockDemo[30046:324146] read
    2018-08-15 17:50:49.885442+0800 iOS-LockDemo[30046:324148] read
    2018-08-15 17:50:50.889361+0800 iOS-LockDemo[30046:324148] write
    2018-08-15 17:50:51.894104+0800 iOS-LockDemo[30046:324148] read
    2018-08-15 17:50:51.894104+0800 iOS-LockDemo[30046:324146] read
    

    由结果可知,打印完write之后,方法每次都是一个一个执行的,而read是可以同时执行的,但是遇到写的操作,就会把其他读或者写都会暂停,也就是说起到了栅栏的作用。

    dispatch_group_t调度组

    前面说了这么多关于锁的使用,其实调度组也能达到类似栅栏的效果。

    api

    //1.创建调度组
    dispatch_group_t group = dispatch_group_create();
    //2.队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    //3.调度组监听队列 标记开始本次执行
    dispatch_group_enter(group);
    //标记本次请求完成
    dispatch_group_leave(group);
    
    //4,调度组都完成了
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //执行刷新UI等操作
    });
    

    eg:

    YZDispatchGroup继承YZBaseLockotherTest里面进行测试,假设的场景是,需要在子线程下载两个图片,sleep()模拟耗时操作,都下载完成之后,回到主线程刷新UI.

    #import "YZDispatchGroup.h"
    
    @implementation YZDispatchGroup
    - (instancetype)init
    {
        self = [super init];
        if (self) {
    
        }
        return self;
    }
    
    - (void)otherTest{
    
        //1.创建调度组
        dispatch_group_t group = dispatch_group_create();
        //2.队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
        //3.调度组监听队列 标记开始本次执行
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
           [self downLoadImage1];
            //标记本次请求完成
              dispatch_group_leave(group);
        });
    
        //3.调度组监听队列 标记开始本次执行
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            [self downLoadImage2];
            //标记本次请求完成
            dispatch_group_leave(group);
        });
    
        //4,调度组都完成了
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            //执行完test1和test2之后,在进行请求test3
             [self reloadUI];
        });
    
    }
    - (void)downLoadImage1 {
        sleep(1);
        NSLog(@"%s--%@",__func__,[NSThread currentThread]);
    }
    - (void)downLoadImage2 {
         sleep(2);
        NSLog(@"%s--%@",__func__,[NSThread currentThread]);
    }
    - (void)reloadUI
    {
        NSLog(@"%s--%@",__func__,[NSThread currentThread]);
    }
    @end
    

    调用的时候

    YZBaseLock *lock = [[YZBarrier alloc] init];
    [lock otherTest];
    

    输出结果为:

    2018-08-15 19:08:35.651955+0800 iOS-LockDemo[3353:49583] -[YZDispatchGroup downLoadImage1]--<NSThread: 0x6000033ed380>{number = 3, name = (null)}
    2018-08-15 19:08:36.648922+0800 iOS-LockDemo[3353:49584] -[YZDispatchGroup downLoadImage2]--<NSThread: 0x6000033e0000>{number = 4, name = (null)}
    2018-08-15 19:08:36.649179+0800 iOS-LockDemo[3353:49521] -[YZDispatchGroup reloadUI]--<NSThread: 0x6000033865c0>{number = 1, name = main}
    

    由结果可知,子线程耗时操作,现在图片时候,主线程刷新UI不执行的,等两个图片都下载完成,才回到主线程刷新UI.

    dispatch_group有两个需要注意的地方

    • dispatch_group_enter必须在dispatch_group_leave之前出现
    • dispatch_group_enter和dispatch_group_leave必须成对出现

    自旋锁,互斥锁的选择

    前面这么多锁,那么到底平时开发中怎么选择呢?其实主要参考如下标准来选择。

    什么情况使用自旋锁比较划算?

    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
    • CPU资源不紧张
    • 多核处理器

    什么情况使用互斥锁比较划算?

    • 预计线程等待锁的时间较长
    • 单核处理器
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈

    参考资料

    本文资料下载github

    OSSpinLock Is Unsafe

    不再安全的 OSSpinLock

    GNUstep源码地址

    Runtime源码

    iOS底层原理

    关于 @synchronized,这儿比你想知道的还要多

    关于iOS中的13种加锁方案(上)

    给大家推荐一个iOS技术交流群,群内提供数据结构与算法、底层进阶、swift、逆向、底层面试题整合文档等免费资料!!!可以加本人微信拉你进群!

    相关文章

      网友评论

        本文标题:关于iOS中的13种加锁方案(下)

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