并发知识的四个层次(Conceptual Overview)
这份文档不讲具体 API,而是从体系结构角度,把并发与多线程知识拆成四个层次,帮助你知道:现在自己在学的是哪一层,还缺哪一层。
第一层:线程本身(Thread as Execution Unit)
这一层只关心一件事:
“线程这个执行单元本身怎么被创建、管理和结束?”
还不讨论共享数据,只是把“工人请进来、安排走、谁负责”弄清楚。
核心内容:
- 线程创建:
std::thread的构造与启动; join/detach:join:等待线程结束,回收资源;detach:后台运行,由运行时回收;
- 生命周期管理:
- 不允许
std::thread析构时仍处于 joinable 状态; - 明确谁负责最后的
join或detach;
- 不允许
- 参数安全传递:
- 值传递、
std::ref引用传递; this指针与对象生命周期;
- 值传递、
- 线程所有权的 RAII 模式:
- 用类封装线程,在构造启动、析构
join/detach(如JoiningThread); - 避免忘记收线程、避免“野线程”。
- 用类封装线程,在构造启动、析构
这一层打牢之后,你至少能做到:
- 不会随手开线程就扔;
- 每个线程都有“主人”,生命周期路径清晰可见。
第二层:共享数据的正确性(Correct Shared-State Concurrency)
一旦多个线程开始共享数据,立刻会遇到核心问题:
“多个线程同时碰同一份数据,怎么保证不会读到脏数据、不会写坏状态?”
这层是并发体系中最关键也是最容易踩雷的一层。
核心内容:
-
数据竞争与未定义行为(data races & UB)
- data race = 多线程无同步同时读写同一内存位置;
- 一旦 data race,行为就是未定义。
-
互斥量与锁(mutex types & usage)
std::mutex/std::timed_mutex/std::recursive_mutex/std::shared_mutex;- 正确加锁/解锁的基本规则(单一所有权、成对出现等)。
-
RAII 锁封装(lock_guard & unique_lock)
std::lock_guard:简单作用域锁,构造锁定、析构解锁;std::unique_lock:更灵活,可解锁/重锁,可配合condition_variable使用。
-
读写锁(shared_mutex & reader-writer locking)
- 多读单写模型:多线程同时读,一个线程独占写;
- 适合“读远多于写”的共享状态。
-
条件变量(condition_variable & wait/notify)
- 一个线程等待某个条件成立,由其他线程
notify_one/notify_all唤醒; - 典型场景:队列非空、状态机状态变化。
- 一个线程等待某个条件成立,由其他线程
-
虚假唤醒(spurious wakeups)与正确等待模式
- 从设计上假设:
wait可能在条件不满足时返回; - 正确模式:
cv.wait(lock, predicate)或while (!cond) cv.wait(lock);。
- 从设计上假设:
在这一层,你学会的是:
- 如何用锁、条件变量等原语让“多线程访问同一份数据”在语义上正确;
- 知道哪些写法必然导致 data race / UB。
这层是所有并发代码的安全地基。
第三层:任务协作与结果传递(Task-Level Concurrency)
当线程不再只是“各跑各的”,而是要协作完成任务,就进入第三层:
“任务如何在不同线程间分发?结果如何返回?异常如何跨线程传递?”
从“线程 + 共享状态”升级到“任务系统”:
核心内容:
-
future/promise/ 异步任务(async tasks)std::promise在一个线程里设置结果,std::future在另一个线程里取结果;std::async提供更高层的任务提交接口,返回future。
-
异常跨线程传递(exception propagation across threads)
- 在线程函数内部
catch异常,用promise.set_exception(std::current_exception())传递; - 或使用
std::async自动捕获/转发异常,在future.get()时重新抛出。
- 在线程函数内部
-
工作队列与任务调度(work queues & scheduling)
- 生产者–消费者模式:任务入队 → 工作线程出队并执行;
- 基本线程池模型:固定线程数 + 共享任务队列;
- 任务调度策略:FIFO / 优先级 / 分层队列等(概念层面)。
这一层的重点从:
- “保证共享数据不坏掉”
转向: - “让线程组成一个能接任务、出结果的协作系统”。
第四层:系统级风险与性能(System-Level Risks & Performance)
当系统能正确跑起来之后,下一步才是:
“如何避免系统卡死?如何在多核上跑得稳、跑得快?”
这一层关注的是全局行为和性能特征。
核心内容:
-
死锁 / 活锁 / 饥饿(deadlock, livelock, starvation)
- 死锁:互相等待,系统不再前进;
- 活锁:线程在不断重试/退让,但实际工作没有推进;
- 饥饿:某些线程/任务长期拿不到资源或时间片。
-
竞争与扩展性(contention reduction techniques)
- 所有线程都抢一把大锁 → 吞吐量无法随核数线性扩展;
- 分段锁(sharding/striping)、局部缓冲、读写分离、无锁结构等手段减小热点。
-
伪共享与缓存对齐(false sharing & cache alignment)
- 不同线程修改不同变量,却落在同一 cache line,导致频繁缓存失效;
- 通过
alignas或 padding 把热点字段分散到不同 cache line。
这一层讨论的是:
- 系统整体是否在前进(有没有卡死、饿死);
- 在高负载、多核环境下,资源是否被合理利用,性能是否可扩展。
四层之间的连接关系(从“有线程”到“好系统”)
把这四层串起来,是一条自然的成长路径:
第一步:先有线程(Execution Units)
没有线程,就没有并发:
- 学会
std::thread创建 /join/detach; - 理清生命周期与参数传递;
- 用 RAII 包装线程所有权。
这一层告诉你:“工人怎么来,怎么走,谁负责。”
第二步:线程开始共享数据(Shared State Correctness)
一旦有共享数据,就会有:
- data race、脏读、写坏状态的风险。
于是必须掌握:
- 互斥量(mutex);
lock_guard/unique_lock;- 读写锁(
shared_mutex); - 条件变量与等待语义。
这一层解决的是:“多个人怎么安全地动同一张表。”
第三步:线程开始协作完成任务(Task Collaboration)
当你不再满足于“几个线程各跑各的”,而是希望:
- 一个线程生产任务;
- 多个线程消费任务;
- 结果能回到调用方;
- 异常也能被处理;
就会用到:
future/promise/async;- 工作队列与任务调度。
这一层是在打造一个任务系统:
“不只是有工人和工具,而是有任务单、流水线和回执。”
第四步:系统开始变大、变复杂(System-Level Concerns)
线程数增加、锁变多、任务流转变复杂后,才会暴露出:
- 死锁 / 活锁 / 饥饿;
- 锁竞争成为瓶颈;
- 伪共享导致 cache 被打爆。
这时才需要:
- 分析锁图、统一锁顺序,消除死锁;
- 通过分片锁、局部缓冲、无锁结构降低竞争;
- 通过 cache 对齐等手段减少伪共享。
这一层关心的是:
“整个工厂是不是在持续高效地运转,而不是某个车间自己忙得很开心。”
总结:你在并发体系里的“当前坐标”
遇到并发相关问题时,可以先判断它属于哪一层:
- 若是在纠结
join/detach/ 生命周期:→ 第一层; - 若是共享变量读写混乱、偶尔崩溃:→ 第二层;
- 若是在搭线程池、任务队列、异步调用链:→ 第三层;
- 若是系统在高负载下卡死、抖动、扩展性差:→ 第四层。
按这个层次来整理你的知识和实践经验,可以避免在还没解决“共享数据正确性”的时候,就过早陷入“如何实现 lock-free 队列”之类的高阶细节中。