iOS多线程:锁

锁是什么?

锁是一种保证多线程并发执行安全的方式,避免同一时间多个线程同时读取并修改一个值而产生混乱。

例子:火车站多窗口卖票

加锁最重要的就是保证锁是同一把,才能加锁成功。

锁的分类

从大的方向讲有两种锁:互斥锁和自旋锁。

互斥锁和自旋锁的对比:

  • 互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁(休眠)。一旦被访问的资源被解锁,等待资源的线程会被唤醒。
  • 自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁(忙等)。一旦被访问的资源被解锁,等待资源的线程会立即执行。

自旋锁的特点

  1. 自旋锁的性能高于互斥锁,因为响应速度快
  2. 自旋锁虽然会一直自旋等待获取锁,但不会一直占用CPU,超过了操作系统分配的时间片会被强制挂起
  3. 自旋锁如果不能保证所有线程都是同一优先级,则可能会造成死锁(拿不到锁)。

自旋锁和互斥锁的适用场景

  1. 优先使用自旋锁的场景:
    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)经常被调用,但是竞争情况很少
    • CPU资源不紧张
    • 多核处理器
  2. 优先使用互斥锁的场景:
    • 预计线程等待所的时间较长
    • 单核处理器
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈

性能比较

自旋锁、信号量、普通锁、递归锁、条件锁、@synchronized、atomic

锁的使用

自旋锁OSSpinLock

  • 等待锁的线程会处于忙等状态,等待其他线程释放锁,一直占用CPU资源;

  • 不需要手动销毁;

  • 在iOS10被弃用,因为它存在优先级反转的问题。

    优先级反转: 高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。 如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放锁。

os_unfair_lock

(互斥锁,iOS10以后取代OSSpinLock的锁)

  • os_unfair_lock是个low level lock(低级锁),等待os_unfair_lock锁的线程会处于休眠状态。

  • 使用: #import <os/lock.h>

pthread_mutex

  • 是C底层的互斥锁。
  • 属性可以传空(NULL)
  • 需要 #import <pthread.h>
  • 需要手动销毁

pthread_mutex-递归锁

  • 允许重复加锁
  • 加锁次数跟解锁次数要保持一致
  • 同一条线程才能使用递归锁,多个线程时仍然会等待锁的释放

pthread_mutex-条件锁

NSLock、NSRecursiveLock

NSCondition(对mutex和cond的封装)

NSConditionLock

对NSCondition的进一步封装,可以设置具体的条件值

dispatch_semaphore 信号量

  • 信号量的初始值,可以用来控制线程并发访问的最大数量;
  • 信号量的初始值为1,代表同时只允许一条线程访问资源,保证线程同步
  • 在没有等待情况出现时,它的性能比pthread_mutex还要高;但一旦有等待情况出现时,性能就会下降很多。
  • 相比OSSpinLock,它的优势在于等待时不会消耗CPU资源。

@synchronize(对mutex的封装)

@synchronized(object)中,object是一个互斥信号量。

  • @synchronized(nil)不起任何作用
  • synchronized中通过将传入的object作为key,到系统维护的一个的哈希表中取出对应的mutex来进行加锁。**
  • 不管是传入什么类型的object,只要是有内存地址,就能启动同步代码块的效果。

直接将self传入@synchronized中,容易导致死锁的出现:

​ 原因是self很可能会被外部对象访问,被用作key来生成锁,两个公共锁交替使用的场景就容易出现死锁。

​ 正确的做法是传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的。

atomic

  • atomic用于保证属性settergetter的原子操作,相当于在settergetter内部加了线程同步的锁。
  • atomic只能保障settergetter内部的操作的原子性,不能保证数据操作的原子性,因此不能保证数据安全。
  • 在iOS设备上settergetter的操作很多,使用atomic性能会比较差。

其他

NSLockNSConditionNSConditionLockNSRecursiveLock

  • 都是基于pthread_mutex封装的面向对象的锁。
  • 都遵守NSLocking协议,该协议提供了加锁和解锁的方法。
  • 初始化NSConditionLock时需要指定当前condition的值,比如初始化的condition设置为0,三个线程加锁时会匹配这个condition,只有是0的才能加锁成功。解锁时也需要给定一个condition,可以自己随便定义,然后继续寻找匹配condition的线程加锁…依此类推直到加解锁完所有任务。根据NSConditionLock的这种特性,可以用来做多线程任务之间的依赖。