Hang Zhengyang

并发知识的四个层次(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 队列”之类的高阶细节中。