美文网首页iOS面试
iOS 开发中使用的各种锁的总结(2)

iOS 开发中使用的各种锁的总结(2)

作者: iOS开发面试题总结 | 来源:发表于2020-11-08 16:03 被阅读0次

    @synchronized

    @synchronized(object) 指令使用的 object 为该锁的唯一标识,只有当标识相同时,才满足互斥,所以如果线程 2 中的 @synchronized(self) 改为 @synchronized(self.view),则线程 2 就不会被阻塞,@synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动释放互斥锁。@synchronized 还有一个好处就是不用担心忘记解锁了。如果在 @synchronized(object) {} 内部 object 被释放或被设为 nil,从测试结果来看,不会产生问题,但如果 object 一开始就是 nil,则失去了加锁的功能。不过虽然 nil 不行,但是 [NSNull null] 是可以的。

    1. objc4-750 版本之前(iOS 12 之前)@synchronized 是一个基于 pthread_mutex_t 封装的递归锁,之后实现则发生了改变,底层的封装变为了 os_unfair_lock。下面验证它,在 @synchronized 打断点,并且打开 Debug-> Debug Workflow -> Always Show Disassembly:
    #pragma mark - Private Methods
    
    - (void)recuresiveAction {
        // ➡️ 在下面 @synchronized 上打断点  
        @synchronized ([self class]) {
            NSLog(@"🌰🌰🌰 count = %d", count);
            if (count > 0) {
                count--;
    
                [self recuresiveAction];
            }
        }
    }
    
    // 汇编 objc_Simple`-[ViewController recuresiveAction]:
    ...
    0x10868fc4b <+43>:  callq  0x108690360               ; symbol stub for: objc_sync_enter // 👈 看到调用了 objc_sync_enter 函数
    ...
    0x10868fcc7 <+167>: callq  0x108690366               ; symbol stub for: objc_sync_exit // 👈 看到调用了 objc_sync_exit 函数
    ...
    复制代码
    

    看到 @synchronized 调用了 objc_sync_enterobjc_sync_exit 函数,下面从 objc4-781 中看一下这两个函数的实现,objc_sync_exitobjc_sync_enter 函数都位于 objc-sync.mm

    作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:834688868,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

    • 文末有惊喜,请一定看到最后!
    // 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(); // 尝试解锁,返回 true 表示解锁成功,否则表示失败
                if (!okay) {
                    result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
                }
            }
        } else {
            // @synchronized(nil) does nothing
        }
    
        return result;
    }
    复制代码
    
    // Begin synchronizing on 'obj'. 
    // Allocates recursive mutex associated with 'obj' if needed.
    // Returns OBJC_SYNC_SUCCESS once lock is acquired.  
    int objc_sync_enter(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
    
        if (obj) {
            // 根据传入的对象,来获取一个锁,所以使用 @synchronized 时传入对象很重要
            SyncData* data = id2data(obj, ACQUIRE);
            ASSERT(data);
            data->mutex.lock(); // 这里使用 data 的 mutex 成员变量执行 lock
        } else {
            // @synchronized(nil) does nothing
            // 传入 nil 则什么也不做
            if (DebugNilSync) {
                _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
            }
            objc_sync_nil();
        }
    
        return result;
    }
    复制代码
    

    SyncData 定义:

    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;
    复制代码
    

    recursive_mutex_t 是使用 using 关键字声明的模版类:using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>; 下面看一下 recursive_mutex_tt 底层结构:

    template <bool Debug>
    class recursive_mutex_tt : nocopy_t {
        // 底层封装的是 os_unfair_recursive_lock
        os_unfair_recursive_lock mLock;
    
      public:
        constexpr recursive_mutex_tt() : mLock(OS_UNFAIR_RECURSIVE_LOCK_INIT) {
            lockdebug_remember_recursive_mutex(this);
        }
    
        constexpr recursive_mutex_tt(const fork_unsafe_lock_t unsafe)
            : mLock(OS_UNFAIR_RECURSIVE_LOCK_INIT)
        { }
    
        void lock()
        {
            lockdebug_recursive_mutex_lock(this);
            os_unfair_recursive_lock_lock(&mLock);
        }
        ...
      };
    复制代码
    

    objc4-723recursive_mutex_tt 定义:

     // 在 objc4-723 版本中 recursive_mutex_tt 的底层结构为
     class recursive_mutex_tt : nocopy_t {
         // 底层封装的是互斥锁 pthread_mutex_t
         pthread_mutex_t mLock;
    
       public:
         recursive_mutex_tt() : mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER) {
             lockdebug_remember_recursive_mutex(this);
         }
    
         recursive_mutex_tt(const fork_unsafe_lock_t unsafe)
             : mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER)
         { }
     ...
     }
    复制代码
    

    继续查看 os_unfair_recursive_lock 底层实现:

    /*!
    * @typedef os_unfair_recursive_lock
    *
    * @abstract
    * Low-level lock that allows waiters to block efficiently on contention.
    *
    * @discussion
    * See os_unfair_lock.
    *
    */
    OS_UNFAIR_RECURSIVE_LOCK_AVAILABILITY
    typedef struct os_unfair_recursive_lock_s {
    
        os_unfair_lock ourl_lock; // 底层为互斥锁 os_unfair_lock 
        uint32_t ourl_count; // 因为 @synchronized 为递归锁,所以需要记录加锁次数
    
    } os_unfair_recursive_lock, *os_unfair_recursive_lock_t;
    复制代码
    

    到这里可以确认了底层是 os_unfair_lock。 然后我们还注意到 OS_UNFAIR_RECURSIVE_LOCK_AVAILABILITY:

    /*! @group os_unfair_recursive_lock SPI
     *
     * @abstract
     * Similar to os_unfair_lock, but recursive.
     * 与 os_unfair_lock 相似,但是是递归的。
     *
     * @discussion
     * Must be initialized with OS_UNFAIR_RECURSIVE_LOCK_INIT
     * 必须使用 OS_UNFAIR_RECURSIVE_LOCK_INIT 进行初始化
     */
    
    #define OS_UNFAIR_RECURSIVE_LOCK_AVAILABILITY \
            __OSX_AVAILABLE(10.14) __IOS_AVAILABLE(12.0) \
            __TVOS_AVAILABLE(12.0) __WATCHOS_AVAILABLE(5.0)
    复制代码
    

    这里表明是 iOS 12.0 之后才是出现的。至此可验证 iOS 12.0@synchronized 是一个封装了 os_unfair_lock 的递归锁(os_unfair_recursive_lock)。

    1. @synchronized(obj){...} 传入一个对象 obj 进行加锁,如果传入空,则不执行操作。

    @synchronized 使用

    #import "ViewController.h"
    
    static int count = 3;
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        __weak typeof(self) _self = self;
        dispatch_async(global_queue, ^{
            __strong typeof(_self) self = _self;
            if (!self) return;
    
            [self recuresiveAction];
        });
    }
    
    #pragma mark - Private Methods
    
    - (void)recuresiveAction {
        @synchronized ([self class]) {
            NSLog(@"🌰🌰🌰 count = %d", count);
            if (count > 0) {
                count--;
    
                [self recuresiveAction];
            }
        }
    }
    
    #pragma mark - dealloc
    - (void)dealloc {
        NSLog(@"🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...");
    }
    
    @end
    
    // 打印结果:
    🌰🌰🌰 count = 3
    🌰🌰🌰 count = 2
    🌰🌰🌰 count = 1
    🌰🌰🌰 count = 0
    
    🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...
    复制代码
    

    dispatch_semaphore

    dispatch_semaphore 是 GCD 用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号量,一个是发送信号。

    dispatch_semaphore 和 NSCondition 类似,都是一种基于信号的同步方式,但 NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)。而 dispatch_semaphore 能保存发送的信号。dispatch_semaphore 的核心是 dispatch_semaphore_t 类型的信号量。

    emsp;dispatch_semaphore_create(1) 方法可以创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始化值为 1。注意,这里的传入参数必须大于等于 0,否则 dispatch_semaphore 会返回 NULL。  dispatch_semaphore_wait(signal, overTime) 方法会判断 signal 的信号值是否大于 0,大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime 时限到了,也会执行后续任务。  dispatch_semaphore_signal(signal) 发送信号,如果没有等待的线程调用信号,则使 signal 信号值加 1(做到对信号的保存)。一个 dispatch_semaphore_wait(signal, overTime)方法会去对应一个 dispatch_semaphore_signal(signal) 看起来像 NSLock 的 lock 和 unlock,其实可以这样理解,区别只在于有信号量这个参数,lock unlock 只能同一时间,一个线程访问被保护的临界区,而如果 dispatcch_semaphore 的信号量初始值为 x,则可以有 x 个线程同时访问被保护的临界区。

    1. 本来是用于控制线程的最大并发数量,我们将并发数量设置为 1 也可以认为是加锁的功能。
    2. 可能会用到的方法:
    3. 初始化 dispatch_semaphore_create() 传入的值为最大并发数量,设置为 1 则达到加锁效果。
    4. 判断信号量的值 dispatch_semaphore_wait() 如果大于 0,则可以继续往下执行(同时信号量的值减去 1),如果信号量的值为 0,则线程进入休眠状态等待(此方法的第二个参数就是设置要等多久,一般是使用永久 DISPATCH_TIME_FOREVER)。
    5. 释放信号量 dispatch_semaphore_signal() 同时使信号量的值加上 1

    dispatch_semaphore 使用

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @property (nonatomic, assign) NSInteger sum;
    @property (nonatomic, strong) dispatch_semaphore_t semaphore;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.sum = 0;
        self.semaphore = dispatch_semaphore_create(1);
    
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        __weak typeof(self) _self = self;
        dispatch_async(global_queue, ^{
            __strong typeof(_self) self = _self;
            if (!self) return;
    
            dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
            for (unsigned int i = 0; i < 10000; ++i) {
                self.sum++;
            }
            dispatch_semaphore_signal(self.semaphore);
            NSLog(@"🍐🍐🍐 %ld", (long)self.sum);
        });
    
        dispatch_async(global_queue, ^{
            __strong typeof(_self) self = _self;
            if (!self) return;
    
            dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
            for (unsigned int i = 0; i < 10000; ++i) {
                self.sum++;
            }
            dispatch_semaphore_signal(self.semaphore);
            NSLog(@"🍎🍎🍎 %ld", (long)self.sum);
        });
    }
    
    #pragma mark - dealloc
    - (void)dealloc {
        NSLog(@"🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...");
    }
    
    @end
    
    // 打印结果:
    🍐🍐🍐 10000
    🍎🍎🍎 20000
    🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...
    复制代码
    

    pthread_rwlock_t

    学习 pthread_rwlock_t 读写锁之前,首先引入一个问题:“如何实现一个多读单写的模型?”,需求如下:

    • 同时可以有多个线程读取。
    • 同时只能有一个线程写入。
    • 同时只能执行读取或者写入的一种。

    首先想到的就是我们的 pthread_rwlock_t

    1. 读取加锁可以同时多个线程进行,写入同时只能一个线程进行,等待的线程处于休眠状态。

    2. 可能会用到的方法:

    3. pthread_rwlock_init() 初始化一个读写锁

    4. pthread_rwlock_rdlock() 读写锁的读取加锁

    5. pthread_rwlock_wrlock() 读写锁的写入加锁

    6. pthread_rwlock_unlock() 解锁

    7. pthread_rwlock_destroy() 销毁锁

    pthread_rwlock_t 使用

    代码示例,测试代码主要看,打印读取可以同时出现几个,打印写入同时只会出现一个。

    #import "ViewController.h"
    #import <pthread.h>
    
    @interface ViewController ()
    
    @property (nonatomic, assign) pthread_rwlock_t lock;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
    
        [self rwlockType];
    }
    
    #pragma mark - Private methods
    - (void)rwlockType {
        pthread_rwlock_init(&self->_lock, NULL);
    
        dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        __weak typeof(self) _self = self;
        for (unsigned int i = 0; i < 100; ++i) {
            // 同时创建多个线程进行写入操作
            dispatch_async(globalQueue, ^{
                __weak typeof(_self) self = _self;
                if (!self) return;
    
                [self lockWriteAction];
            });
    
            dispatch_async(globalQueue, ^{
                __weak typeof(_self) self = _self;
                if (!self) return;
    
                [self lockWriteAction];
            });
    
            dispatch_async(globalQueue, ^{
                __weak typeof(_self) self = _self;
                if (!self) return;
    
                [self lockWriteAction];
            });
    
            // 同时创建多个线程进行读操作
            dispatch_async(globalQueue, ^{
                __strong typeof(_self) self = _self;
                if (!self) return;
    
                [self lockReadAction];
            });
    
            dispatch_async(globalQueue, ^{
                __strong typeof(_self) self = _self;
                if (!self) return;
    
                [self lockReadAction];
            });
    
            dispatch_async(globalQueue, ^{
                __strong typeof(_self) self = _self;
                if (!self) return;
    
                [self lockReadAction];
            });
        }
    }
    
    - (void)lockReadAction {
        pthread_rwlock_rdlock(&self->_lock);
        sleep(1);
        NSLog(@"RWLock read action %@", [NSThread currentThread]);
        pthread_rwlock_unlock(&self->_lock);
    }
    
    - (void)lockWriteAction {
        pthread_rwlock_wrlock(&self->_lock);
        sleep(1);
        NSLog(@"RWLock Write Action %@", [NSThread currentThread]);
        pthread_rwlock_unlock(&self->_lock);
    }
    
    #pragma mark - dealloc
    
    -(void)dealloc {
        NSLog(@"🚚🚚🚚 deallocing...");
    
        pthread_rwlock_destroy(&self->_lock);
    }
    
    @end
    // 打印结果: 可看到每次 write 操作同一个时间只执行一次,每次执行 write 操作至少相差 1 的时间,而 read 操作,几乎三次读取完全同一时刻进行
    2020-08-23 21:56:47.918292+0800 algorithm_OC[17138:583665] RWLock Write Action <NSThread: 0x600001d45440>{number = 6, name = (null)}
    2020-08-23 21:56:48.918953+0800 algorithm_OC[17138:583666] RWLock Write Action <NSThread: 0x600001d58740>{number = 4, name = (null)}
    2020-08-23 21:56:49.924037+0800 algorithm_OC[17138:583667] RWLock Write Action <NSThread: 0x600001d06440>{number = 3, name = (null)}
    
    2020-08-23 21:56:50.927716+0800 algorithm_OC[17138:583697] RWLock read action <NSThread: 0x600001d00d40>{number = 10, name = (null)}
    2020-08-23 21:56:50.927716+0800 algorithm_OC[17138:583696] RWLock read action <NSThread: 0x600001d864c0>{number = 8, name = (null)}
    2020-08-23 21:56:50.927721+0800 algorithm_OC[17138:583698] RWLock read action <NSThread: 0x600001da4b40>{number = 9, name = (null)}
    ...
    复制代码
    

    dispatch_barrier_async 实现多读单写

    1. 传入的并发队列必须是手动创建的,dispatch_queue_create() 方式,如果传入串行队列或者通过 dispatch_get_global_queue() 方式创建,则 dispatch_barrier_async 的作用就跟 dispatch_async 变的一样。
    2. 可能会用到的方法:
    3. dispatch_queue_create() 创建并发队列
    4. dispatch_barrier_async() 异步栅栏

    dispatch_barrier_async 使用

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @property (nonatomic, strong) dispatch_queue_t queue;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
    
        [self barrierAsyncType];
    }
    
    #pragma mark - Private methods
    - (void)barrierAsyncType {
        self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
        for (unsigned int i = 0; i < 100; ++i) {
    
            // 同时创建多个线程进行写入操作
            [self barrierWriteAction];
            [self barrierWriteAction];
            [self barrierWriteAction];
    
            // 同时创建多个线程进行读取操作
            [self barrierReadAction];
            [self barrierReadAction];
            [self barrierReadAction];
        }
    }
    
    - (void)barrierReadAction {
        dispatch_async(self.queue, ^{
            sleep(1);
            NSLog(@"barrier Read Action %@", [NSThread currentThread]);
        });
    }
    
    - (void)barrierWriteAction {
        // 写操作使用 dispatch_barrier_async
        dispatch_barrier_async(self.queue, ^{
            sleep(1);
            NSLog(@"barrier Write Action %@", [NSThread currentThread]);
        });
    }
    
    @end
    
    // 打印结果: 从打印时间可以看出,write 操作是依序进行的,每次间隔 1 秒,而 read 操作几乎都是同时进行 3 次
    2020-08-23 22:25:14.144265+0800 algorithm_OC[17695:604062] barrier Write Action <NSThread: 0x6000012a0180>{number = 5, name = (null)}
    2020-08-23 22:25:15.148017+0800 algorithm_OC[17695:604062] barrier Write Action <NSThread: 0x6000012a0180>{number = 5, name = (null)}
    2020-08-23 22:25:16.151869+0800 algorithm_OC[17695:604062] barrier Write Action <NSThread: 0x6000012a0180>{number = 5, name = (null)}
    
    2020-08-23 22:25:17.156004+0800 algorithm_OC[17695:604062] barrier Read Action <NSThread: 0x6000012a0180>{number = 5, name = (null)}
    2020-08-23 22:25:17.156040+0800 algorithm_OC[17695:604063] barrier Read Action <NSThread: 0x600001230340>{number = 6, name = (null)}
    2020-08-23 22:25:17.156023+0800 algorithm_OC[17695:604065] barrier Read Action <NSThread: 0x6000012e6300>{number = 3, name = (null)}
    ...
    复制代码
    

    总结

    锁粗略的效率排序(不同的锁可能更擅长不同的场景)

    1. os_unfair_lock (iOS 10 之后)
    2. OSSpinLock (iOS 10 之前)
    3. dispatch_semaphore (iOS 版本兼容性好)
    4. pthread_mutex_t (iOS 版本兼容性好)
    5. NSLock (基于 pthread_mutex_t 封装)
    6. NSCondition (基于 pthread_mutex_t 封装)
    7. pthread_mutex_t(recursive) 递归锁的优先推荐
    8. NSRecursiveLock (基于 pthread_mutex_t 封装)
    9. NSConditionLock (基于 NSCondition 封装)
    10. @synchronized
    11. iOS 12 之前基于 pthread_mutex_t 封装
    12. iOS 12 之后基于 os_unfair_lock 封装(iOS 12 之后它的效率应该不是最低,应该在 3/4 左右)

    自旋锁和互斥锁的取舍  自旋锁和互斥锁怎么选择,其实这个问题已经没有什么意义,因为自旋锁 OSSpinLockiOS 10 之后已经废弃了,而它的替换方案 os_unfair_lock 是互斥锁,但是我们仍然做一下对比: 自旋锁:

    • 预计线程需要等待的时间较短
    • 多核处理器
    • CPU 的资源不紧张

    互斥锁:

    • 预计线程需要等待的时间较长
    • 单核处理器
    • 临界区(加锁解锁之间的部分)有 I/O 操作

    其它: 加锁和解锁的实现一定要配对出现,不然就会出现阻塞死锁的现象。

    参考链接

    参考链接:🔗

    作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:834688868,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

    以下资料在群文件可自行下载!

    相关文章

      网友评论

        本文标题:iOS 开发中使用的各种锁的总结(2)

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