iOS多线程:锁
锁是什么?
锁是一种保证多线程并发执行安全的方式,避免同一时间多个线程同时读取并修改一个值而产生混乱。
例子:火车站多窗口卖票
加锁最重要的就是保证锁是同一把,才能加锁成功。
锁的分类
从大的方向讲有两种锁:互斥锁和自旋锁。
互斥锁和自旋锁的对比:
- 互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁(休眠)。一旦被访问的资源被解锁,等待资源的线程会被唤醒。
- 自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁(忙等)。一旦被访问的资源被解锁,等待资源的线程会立即执行。
自旋锁的特点
- 自旋锁的性能高于互斥锁,因为响应速度快
- 自旋锁虽然会一直自旋等待获取锁,但不会一直占用CPU,超过了操作系统分配的时间片会被强制挂起
- 自旋锁如果不能保证所有线程都是同一优先级,则可能会造成死锁(拿不到锁)。
自旋锁和互斥锁的适用场景
- 优先使用自旋锁的场景:
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但是竞争情况很少
- CPU资源不紧张
- 多核处理器
- 优先使用互斥锁的场景:
- 预计线程等待所的时间较长
- 单核处理器
- 临界区有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
用于保证属性setter
、getter
的原子操作,相当于在setter
、getter
内部加了线程同步的锁。atomic
只能保障setter
、getter
内部的操作的原子性,不能保证数据操作的原子性,因此不能保证数据安全。- 在iOS设备上
setter
和getter
的操作很多,使用atomic
性能会比较差。
其他
NSLock
、NSCondition
、NSConditionLock
、NSRecursiveLock
- 都是基于
pthread_mutex
封装的面向对象的锁。 - 都遵守
NSLocking
协议,该协议提供了加锁和解锁的方法。 - 初始化
NSConditionLock
时需要指定当前condition
的值,比如初始化的condition
设置为0,三个线程加锁时会匹配这个condition,只有是0的才能加锁成功。解锁时也需要给定一个condition
,可以自己随便定义,然后继续寻找匹配condition
的线程加锁…依此类推直到加解锁完所有任务。根据NSConditionLock
的这种特性,可以用来做多线程任务之间的依赖。