多线程- 锁 线程问题? 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(),所以系统也无法知道其所有者是谁,也存在优先级反转问题