iOS 锁相关

锁分类

5f153aa9289ca8dab6e640c4a1b870b9

风险

在主线程连续调用 [self.lock lock] 会导致主线程死锁 在子线程连续 [self.lock lock] 会导致死锁,同时别的子线获取 self.lock 则会一直等待下去。 同时子线程死锁会导致 ViewController 不释放。

死锁的四个条件

  • 禁止抢占: 系统资源不能被强制从一个进程中退出
  • 持有和等待: 一个进程可以在等待时持有系统资源
  • 互斥: 资源只能同步分配给一个进程,无法共享
  • 循环等待: 进行间互相持有其他进行需要的资源

死锁只有在四个条件都满足才会发生

什么是优先级反转

  • 如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock,这就是优先级反转。这并不只是理论上的问题,开发者已经遇到很多次这个问题,于是苹果工程师停用了 OSSpinLock。

    除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。

这个解释更好 高优先级和低优先级的任务之间共享资源时,就可能发生优先级反转。当低优先级的任务获得了共享资源的锁时,该任务应该迅速完成,并释放掉锁,这样高优先级的任务就可以在没有明显延时的情况下继续执行。然而高优先级任务会在低优先级的任务持有锁的期间被阻塞。如果这时候有一个中优先级的任务(该任务不需要那个共享资源),那么它就有可能会抢占低优先级任务而被执行,因为此时高优先级任务是被阻塞的,所以中优先级任务是目前所有可运行任务中优先级最高的。此时,中优先级任务就会阻塞着低优先级任务,导致低优先级任务不能释放掉锁,这也就会引起高优先级任务一直在等待锁的释放

自旋锁为什么不安全

  • 优先级翻转

常规锁介绍

  • @synchronized 结构在工作时为传入的对象分配了一个递归锁, 性能差, 调用 sychronized 传入的每个对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。

  • NSLock实现了最基本的互斥锁,遵循NSLocking协议,通过lock和unLock来进行锁定于解锁。当一个线程访问的时候,该线程获得锁,其他线程访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而确保线程安全。如果连续锁定,则会造成死锁问题。
  • NSRecursiveLock递归锁 可以在同一个线程中重复加锁也不会导致死锁。NSRecursiveLock 会记录加锁和解锁的次数,当二者次数相等时,此线程才会释放锁,其它线程才可以上锁成功。
  • NSConditionLock条件锁
  • NSCondition 是一种特殊类型的锁,通过它可以实现不同线程的调度。A线程被某一个条件所阻塞,直到B线程满足该条件,从而发送信号给A线程使得A线程继续执行,类似信号量
  • OSSpinLock 是一种自旋锁,和互斥锁类似,会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。
  • 互斥锁:当一个线程获得此锁之后,会先轮询,但一秒后便会使线程进入 waiting 状态,等待唤醒
  • 自旋锁:当一个线程获得此锁之后,其他线程将会一直循环查看该锁是否被释放。锁比较适用于锁的持有者保存时间较短的情况下。
  • os_unfair_lock (互斥锁的一种) 从 iOS 10 之后开始支持,跟 OSSpinLock 不同,等待 os_unfair_lock 的线程会处于休眠状态(类似 Runloop 那样),不是忙等(busy-wait)。
  • NSDistributedLock 分布式锁 处理多个进程或多个程序之间互斥问题。一个获取锁的进程或程序在是否锁之前挂掉,锁不会被释放,可以通过breakLock方式解锁。
  • pthread_rwlock_t 读写锁
  • pthread_mutex_t 是 C 语言下多线程互斥锁的方式,是跨平台使用的锁,等待锁的线程会处于休眠状态,可根据不同的属性配置把 pthread_mutex_t 初始化为不同类型的锁,例如:互斥锁、递归锁、条件锁。 相关方法:

      - pthread_mutex_destroy(&self->_lock); 销毁锁(dealloc)
      - pthread_mutexattr_t att;  pthread_mutexattr_init(&att); // 初始化属性
      - pthread_mutexattr_settype(&att, PTHREAD_MUTEX_DEFAULT); // 设置属性,描述锁是什么类型
      - pthread_mutex_init(&self->_lock, &att); // 初始化锁
      - pthread_mutexattr_destroy(&att); // 销毁属性
      - pthread_mutex_lock(&self->_lock) // 加锁(unlock解锁)
      - pthread_cond_wait(&self->_condition, &self->_lock); 条件锁等待
      - pthread_cond_signal(&self->_condition); 条件锁发信号
    

原理

mutex底层有实现一个阻塞队列,如果当前有其他任务正在执行,则加入到队列中,放弃当前cpu时间片。一旦其他任务执行完,则从队列中取出等待执行的线程对象,恢复上下文重新执行。

@synchronized会变成一对基于try-catch的objc_sync_enter和objc_sync_exit的代码。

 @try {
    objc_sync_enter(synchronizeTarget);
    //code 
} @finally {
    objc_sync_exit(synchronizeTarget);   
}

int objc_sync_enter(id obj) {
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData *data = id2data(obj, ACQUIRE);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        objc_sync_nil();
    }
    return result;
}

查看objc_sync_enter源码可以看出, synchronized 中传入的obj的内存地址被用作key,通过hash map获取系统维护的一个递归锁。

锁的源码

读写锁

读写锁实际是一种特殊的自旋锁。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

6.1 dispatch_barrier_async / dispatch_barrier_sync 共同点:1、等待在它前面插入队列的任务先执行完;2、等待他们自己的任务执行完再执行后面的任务。 不同点:1、dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们;2、dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入队列,然后等待自己的任务结束后才执行后面的任务。

读写锁

// 初始化锁
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);

atomic

系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。 Atomic不能保证对象多线程的安全。所以Atomic 不能保证对象多线程的安全。它只是能保证你访问的时候给你返回一个完好无损的Value而已。举个例子: 如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,有3种可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。所以atomic可并不能保证对象的线程安全。