线程活性问题
死锁活锁和饥饿是破坏系统活跃性的主要三种问题.
死锁
多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力干涉,它们都将无法推进
死锁的存在有四大必要条件 ——
- 互斥 (Mutual Exclusion): 资源在同一时刻只能被一个线程占用。
- 占有并等待 (Hold and Wait): 线程在持有至少一个资源的同时,请求获取其他被占用的资源。
- 非抢占 (No Preemption): 资源不能被强制从占用它的线程中剥夺,只能由占有它的线程主动释放。
- 循环等待 (Circular Wait): 存在一个线程等待环路,如 T1 等待 T2 占用的资源,T2 等待 T1 占用的资源。
在发生死锁的时候,大部分情况我们无能为力.所以更重要的是设计出不会发生死锁的代码 —— 对发生死锁的四大条件下手
- 破坏循环等待 —— 强制全局锁排序 —— 强制系统中的所有线程按照固定的、统一的顺序获取锁(例: 始终先加锁标识符小的,再加锁标识符大的)
- 使用定时锁
- 等
双缓冲
(思想很不错,所以再说说,在图形那块保证没有画面撕裂也是使用类似的思想)
维护了两个 Buffer块 A 和 B, 同时指定 A 为”只读“; B 为“只写”. 用这样的操作把读写操作解耦.在旧的模型中,数据只有一份,所以在读取数据的时候需要加锁;在写入数据的时候也需要加锁;就可能出现死锁问题.现在完全分离 —— 保证不会出现生产者和消费者抢夺同一资源的问题.
只读内存块 A 表示当前最新的数据,不妨定义其为t时刻的数据,那么同一时间,我们让只写内存块B被刷入数据,同时记作t+1时刻的数据(未来人这块).
然后在t时刻结束的时候, 我们强制等待B块的数据也写入完毕,此时只需要交换A和B的地址,就可以使得t+1时刻的数据作为下一帧的最新用来读取的数据
**边界约束:** 双缓冲架构**仅**解决“读-写 (Reader-Writer)”死锁与冲突。在 B 块(Write View)内部,如果存在多个逻辑线程并发写入,**仍需**配合数据分片(Partitioning)或无锁队列,否则 B 块内部依然会发生“写-写 (Writer-Writer)”数据竞争。
活锁
活锁发生时,多个并发线程并没有被操作系统挂起(阻塞),它们仍在不断地执行代码、响应其他线程的状态变化,并尝试推进任务。然而,由于所有涉及的线程都在执行绝对对称的重试逻辑,导致它们的状态不断发生碰撞和回退。系统作为一个整体,有效吞吐量(Goodput)降为零。
看到比较形象的例子 —— 狭路,迎面有人,此时双方都在左右避让(正常运行),但是怎么移动都刚好碰到一起(无法成功获得资源)而无法往前继续行进.
特征也非常明显 ——
- CPU 占用率极高: 陷入活锁的线程通常处于极速的自旋循环(Spin Loop)中,单核或多核占用率飙升至 100%。
- 状态高频震荡: 线程内部的状态机在“尝试获取资源”与“释放资源并退避”之间高频切换。
- 无确定性进展: 与死锁的绝对静止不同,活锁是动态的,但在宏观上任务毫无进度。
所以,解决活锁的核心思想是打破对称性 (Break Symmetry) 并降低争抢频率 (Reduce Contention)
(实际上感觉都是利用引入随机性等来打破对称)
比如截断二进制指数退避, 当一个线程第c次尝试 CAS 失败时,它不能立刻重试,而是必须等待一段随机时间.(通过随机性来引入非对称性,同时这个随机性随时间指数增长)
饥饿
饥饿是指一个处于就绪状态的线程,因为所需资源的分配策略不公平,导致其等待时间无限期延长,甚至永远无法进入临界区执行。
简单来说,就是线程一直等待执行,但是由于优先级不够高,一直处于等待调度器分配等过程中而无法进入执行.
可以通过优化调度器算法进行解决 —— 比如不是单纯的看优先级,也要结合线程发起的时间戳来进行判断.
或者使用公平锁 / 比较公平的调度方式 —— 但是可能会降低效率.
综上所述
| 异常类型 (Liveness Hazard) | 宏观表现 (Macro View) | 微观 CPU 状态 (Micro View) | 核心成因 (Core Cause) | 破局核心哲学 (Resolution Philosophy) |
|---|---|---|---|---|
| 死锁 (Deadlock) | 永久性卡死,系统/子系统彻底失去响应。 | 极低 (≈ 0%)。线程被操作系统挂起进入 Sleep 状态。 | 满足 Coffman 四条件(互斥、占有并等待、不可剥夺、循环等待)。 | 全局秩序与物理隔离。 1. 强制锁排序 (std::scoped_lock) 2. 双缓冲读写隔离 3. 数据分片消除写竞争 |
| 活锁 (Livelock) | 任务吞吐量降为 0,但系统依然在疯狂运转(如风扇狂转)。 | 极高 (≈ 100%)。陷入极速的自旋循环与缓存一致性风暴。 | 多线程执行了高度对称的重试与回退逻辑,步伐完全一致地互相踩踏。 | 打破对称性与硬件降温。 1. 引入随机指数退避 (Random Backoff) 2. 插入硬件暂停指令 (_mm_pause) 3. 混合降级调度 (Spin-to-Yield) |
| 饥饿 (Starvation) | 系统整体运行良好,但极少数特定任务永远无法被执行,产生极长的尾部延迟。 | 正常。霸占资源的线程在正常消耗 CPU。 | 调度器优先级反转、非公平锁的竞争、或读写锁的“偏向性”压制。 | 吞吐量与公平性的权衡。 1. 引入老化机制 (Aging) 动态提权 2. 使用公平锁 (Ticket Lock/MCS) 3. 实现写者优先 (Writer-Preferring) 机制 |