文章

iOS中的锁

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

锁的分类

在iOS中锁的基本种类只有两种:互斥锁自旋锁,其他的比如条件锁递归锁信号量都是上层的封装和实现

互斥锁

互斥锁(Mutual exclusion,缩写Mutex)防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒

互斥锁又分为:

  • 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用
  • 非递归锁:不可重入,必须等锁释放后才能再次获取锁

自旋锁

自旋锁(Spinlock)是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。 由于线程在这一过程中保持执行,因此是一种忙等待。 一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。

⾃旋锁避免了进程上下⽂的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

iOS中常见锁

  1. @synchronized 关键字加锁
  2. NSLock 对象锁
  3. NSCondition
  4. NSConditionLock 条件锁
  5. NSRecursiveLock 递归锁
  6. pthread_mutex 互斥锁(C语言)
  7. dispatch_semaphore 信号量实现加锁(GCD)
  8. OSSpinLock 自旋锁
  9. pthread_rwlock
  10. POSIX Conditions
  11. os_unfair_lock iOS10之后替代OSSPinLock的锁,解决了优先级反转的问题

@synchronized

@synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(可以理解为锁池),通过对象的哈希值来得到对应的互斥锁。1

@synchronized 在幕后做的事情是调用了 objc_sync 中的 objc_sync_enterobjc_sync_exit 方法,并且加入了一些异常判断。2

NSLock

互斥锁的一种,lock和unlock成对出现。

例:

1
2
3
4
NSLock *lock = [[NSLock alloc] init];
[lock lock];
//your code
[lock unlock];

NSCondition

使用NSCondition,实现多线程的同步,即,可实现生产者消费者问题。3

基本思路是,

  • 首先要创建公用的NSCondition实例。

  • 消费者取得锁,取产品,如果没有,则wait,这时会释放锁,直到有线程唤醒它去消费产品;

  • 生产者制造产品,首先也是要取得锁,然后生产,再发signal,这样可唤醒wait的消费者。

NSConditionLock

NSConditionLock对象所定义的互斥锁可以在使得在某个条件下进行锁定和解锁。它和NSCondition 很像,但实现方式是不同的。 当两个线程需要特定顺序执行的时候,例如生产者消费者模型,则可以使用 NSConditionLock 。当生产者执行执行的时候,消费者可以通过特定的条件获得锁,当生产者完成执行的时候,它将解锁该锁,然后把锁的条件设置成唤醒消费者线程的条件。锁定和解锁的调用可以随意组合lock 和 unlockWithCondition 配合lockWhenCondition 和 unlock 配合使用。

NSRecursiveLock

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

dispatch_semaphore

dispatch_semaphore有三个函数,分别是:

  • dispatch_semaphore_create

    1
    
      dispatch_semaphore_t dispatch_semaphore_create(long value);
    

    传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。 值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。

  • dispatch_semaphore_wait

    1
    
      long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
    

    这个函数会使传入的信号量dsema的值减1; 这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1; 如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t, 不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了, 且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。 如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。

  • dispatch_semaphore_signal

    1
    
      long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
    

    当返回值为0时表示当前并没有线程等待其处理的信号量,其处理的信号量的值加1即可。当返回值不为0时,表示其当前有(一个或多个)线程等待其处理的信号量,并且该函数唤醒了一个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒)。

    dispatch_semaphore_wait的返回值也为long型。当其返回0时表示在timeout之前,该函数所处的线程被成功唤醒。当其返回不为0时,表示timeout发生。

OSSpinLock

OSSpinLock 不再安全 4,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。

os_unfair_lock

  • os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持。

  • 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。

  • 使用时需要导入头文件#import <os/lock.h>

1
2
3
4
5
6
7
8
 // 初始化
 os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
 //尝试加锁
 os_unfair_lock_trylock(&lock);
 // 加锁
 os_unfair_lock_lock(&lock);
 // 解锁
 os_unfair_lock_unlock(&lock);

参考资料

iOS开发中自旋和互斥锁的理解以及所有锁的性能比较

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

本文由作者按照 CC BY 4.0 进行授权