多线程- 锁
线程问题?
1.资源共享
并发编程中许多问题的根源就是在多线程中访问共享资源。资源可以是一个属
性、一个对象,通用的内存、网络设备和文件等等。在多线程中任意共享的资源
都有一个潜在的冲突,开发者必须防止相关冲突的发生
为了防止出现这样的问题,在多线程访问共享资源时,需要一种互斥的机制
线程同步技术
就是加锁技术
iOS 主要是 2种,自旋锁和互斥锁
2.互斥(锁)
同一时刻,只允许一个线程访问某个资源
一个线程首先需要获得一个共享资源的互斥锁。然后等待资源的锁被释放,然后
获得资源读写权限
缺点: 线程切换
互斥锁 在 线程的资源锁 释放,等待,获取 过程中, 是会存在一定的线程切换
消耗的,
CPU消耗优于自旋锁:
自旋锁等待是忙等busy-waite,消耗更多的CUP资源
3.现代CPU 优化
设计锁机制还不够,看下面:
现代CPU访问内存机制可能是乱序的,这又增加来多线程读写难度,使得加锁同
步成为问题
4.内存屏障:
为了解决由CPU的优化策略引起的代码无序执行,需要引入内存屏障(memory
barrier)。通过设置内存屏障,来确保无序执行时能够正确跨越设置的屏障
因此,加锁机制的成功,是依赖与 内存屏障 的
5.获取和释放锁 开销
如果一个线程加锁成功并获取资源,但是其释放之前需要 进行大量计算,那么有
什么问题呢?
其它线程等待成本大幅提高,并发访问整体效率下降
因此,我们只针对需要的地方加锁,不要将耗时计算纳入锁范围,并尽快释放才
行。
6. 发生死锁
互斥解决了资源竞争的问题,但同时这也引入了一个新的问题:死锁
当多个线程在相互等待着对方的结束时,就会发生死锁,这是程序可能会被卡住
主线程下 :同步线程+串行队列。 就是导致死锁的典型案例,线程任务互相等待
主线程 A方法执行,方法内有同步串行方法,这个同步任务B 不能开辟线程,被
加到主队列中,然后 被丢到主线程下运行,而A方法又依赖任务B,任务B又在主
线程的A任务后面,互相等待死锁就发生了
当然还有很多其它子线程上的死锁案例,只要你资源共享越多,并发任务越多,
场景就会越复杂,发生死锁的概率也就会越大。
因此,我们应该
尽量避免过多的并发任务,
尽量减少线程任务的依赖。
尽量减少过瘾复杂的优先级
7. 读写锁(互斥锁)
当一个线程读锁,需要等待另一个线程的读锁 会不会觉得性能很浪费?
饥饿:
大家都是读,还需要等待,会不会觉得很饥饿呢,,迫不及待的感觉
那么读写锁就产生了
大家都是读锁,那就不用等待了,因为读又不会改变数据,结果都是一样的(设
置有写设计缓存的就是为了提高访问速度)
读锁需要等待写锁释放
写锁需要等待读锁释放
读锁不需要等待读锁释放
8.饥饿状态
如果一个线程,去申请写资源时,发现前面有读锁 在,然后就只能干等着了。
如果一个线程去申请读时,发现有写线程在,那么又得等着了。
这种干等着的心态就是饥饿状态。
那么在读写锁的基础上,这种饥饿状态还是可以优化的,比如 给定一个writer
preference,或者使用read-copy-update算法
9. 自旋锁(锁)
自旋锁等待是忙等busy-waite,消耗更多的CUP资源
等待其它线程释放过程因消耗过多的CPU,那么自旋锁一般适合 加锁过程 耗时极
短情况。这样加锁效率更高
10. 优先级
接着我们介绍锁优先级,优先级的存在对 自旋锁 设计来说,将是致命的
优先级:
在资源锁释放的时候,如果大家优先级一样,就按先来后到顺序获取就好
如果优先级不一样呢,一般来说优先级高的获得锁的机会更大
10.优先级反转
一般来说,自旋锁忙等时占用CPU资源 是这个 问题的 根源
举例
线程1 优先级10 , 线程2 优先级30
抢占资源A
线程3 优先级20
抢占资源B
线程1加锁资源A
线程2加锁资源A失败,忙等中
线程3 加锁资源B
线程3优先级高与线程1,抢占CPU
线程1因为CPU资源被抢占,等待中
(线程2 也在等待中)
一段时间后,线程3结束此任务,
释放锁资源B,释放CPU资源
线程1 恢复,处理任务后,释放资源A
线程2获得资源,并处理
线程2完成,释放资源A
从上面的自旋锁例子可以发现,线程2先申请任务,线程3后申请任务,结果线程
3作为低优先级却先完成,这就是 优先级反转。
如果是特别复杂的项目,优先级反转可能会导致不可预估的后果。甚至项目停摆
iOS 自旋锁OSSpinLock 因为优先级反转隐患被弃用,后改用os_unfair_lock
atomic内部也是用自旋锁实现的,但后续也改成互斥锁了
11. QoS 传递
直译文是 :服务质量,表示: 优先级
QoS(Quality of Service),用来指示某任务或队列的运行优先级。
iOS 系统主要使用以下两种机制来在不同线程(或 queue)间传递 QoS:
机制1:dispatch_async
机制2:基于 XPC 的进程间通信(IPC)
系统的 QoS 传递规则比较复杂,主要参考以下信息:
当前线程的 QoS
如果是使用 dispatch_block_create() 方法生成的 dispatch_block,则考虑生成
block 时所调用的参数
dispatch_async 或 IPC 的目标 queue 或线程的 QoS
记录了持有者信息(所有者)的系统API如下:
Pthread mutex、os_unfair_lock、以及基于这些二者实现的其他上层API
--------Dispatch_once的实现是基于os_unfair_lock的
--------NSLock、NSRecursiveLock、@synchronized 等的实现是基于线程
互斥体
Dispatch_sync()
Xpc_connection_send_with_message_sync()
使用以上这些API能够在发生优先反转时使系统启用优先反转避免机制。
Dispatch_semaphroe
Dispatch_semaphroe 无法避免优先级反转的原因
信号量不是一个异步方法,所以它没有QoS的概念
在调用 dispatch_semaphore_wait() 时,系统不知道哪个线程会调用
dispatch_semaphore_signal() 方法,所以系统无法知道所有者信息
Dispatch_group
Dispatch_group 跟 semaphore 类似,在调用 enter() 方法时,无法预知谁会
调用 leave(),所以系统也无法知道其所有者是谁,也存在优先级反转问题