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

2,975 阅读8分钟

@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

// 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 操作

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

参考链接

参考链接:🔗