C++ 里"线程通信"这件事,容易被讲成"mutex / condvar / atomic 三板斧"。但更准确的说法是:
线程通信 = 在共享地址空间内,借助同步原语完成"数据传递"和"状态通知"。
C++11 之后标准库已经形成一套分层体系:底层是原子与互斥,中间是条件变量与信号量,上层是 future / 任务模型,再往上是消息队列、线程池、协程管道等设计模式。下面按"抽象层级 + 通信语义"系统梳理。
一、共享内存:所有通信的物理基础
同一进程内的线程共享地址空间。因此"线程 A 写、线程 B 读"本身就是通信,只是不安全。
int g = 0;
// 线程 A: g = 42;
// 线程 B: printf("%d\n", g);
没有同步时,这是数据竞争,属于未定义行为。
所以 C++ 多线程通信不是"选一种魔法",而是:
共享内存负责传输,同步原语负责排序与可见性。
二、互斥与锁:保护"共享状态"的通信
1. 语义
互斥不直接"传消息",而是保证:
-
临界区内对共享数据的修改,对其他拿到锁的线程可见
-
复合不变式不被并发破坏
std::mutex m;
int balance = 0;void deposit(int x) {
std::lock_guardstd::mutex lk(m);
balance += x; // 对其它加锁线程可见
}
2. 家族
| 原语 | 语义 |
|---|---|
std::mutex |
最基本互斥 |
std::recursive_mutex |
同一线程可重入 |
std::timed_mutex |
支持超时尝试 |
std::shared_mutex(C++17) |
多读单写 |
std::lock_guard |
最简 RAII 锁 |
std::unique_lock |
可延迟/可移动/配 condvar |
std::shared_lock |
读锁 |
std::scoped_lock |
多锁死锁安全 |
3. 适用边界
适合:
-
多字段对象一致性
-
容器、状态机、配置、缓存
-
业务不变式复杂,难以用原子表达
不适合:
-
高频计数器(用 atomic)
-
单纯"等一件事发生"(用 condvar / semaphore)
三、条件变量:事件通知,而不是数据传输
1. 语义模型
条件变量解决的是:
"某个条件成立时,唤醒在等的线程。"
它必须和 mutex 配合使用,保护的是谓词状态,不是数据本身。
std::mutex m;
std::condition_variable cv;
std::queue<int> q;
void producer() {
{
std::lock_guard<std::mutex> lk(m);
q.push(42);
}
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{ return !q.empty(); });
int v = q.front(); q.pop();
}
2. 三个铁律
-
wait必须有谓词,否则丢唤醒 / 假唤醒都会出错 -
notify前通常先修改受保护状态 -
条件变量不携带数据,只携带"状态可能变了"的信号
3. 典型场景
-
生产者 / 消费者
-
任务队列
-
状态机切换
-
"数据就绪"通知
四、原子变量:轻量级状态与无锁通信
1. 语义
std::atomic<T>提供:
-
原子性读写
-
内存序(memory order)
-
跨线程可见性
std::atomic
ready{false};
int data = 0;// 线程 A
data = 42;
ready.store(true, std::memory_order_release);// 线程 B
while (!ready.load(std::memory_order_acquire)) {
// spin / yield
}
// 此处能看到 data == 42
2. 能做什么
-
停止标志:
atomic<bool> stop -
引用计数
-
发布指针 / 数据缓冲区
-
无锁栈、无锁队列、SPSC 通道
3. C++20 的原子等待
std::atomic<int> state{0};
state.wait(0); // 阻塞直到值变化
state.store(1);
state.notify_all();
这比"mutex + condvar"轻,也比忙等高效。
4. 边界
原子适合:
-
单变量
-
标志位
-
计数器
-
发布-订阅式指针
不适合:
-
多字段事务
-
复杂不变式
-
"等 N 个条件同时成立"的复杂协调
五、Future / Promise:一次性结果通信
1. 模型
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t([p = std::move(p)] mutable {
p.set_value(compute());
});
int r = f.get(); // 阻塞等结果或异常
通信内容是:
-
一个返回值
-
或一个异常
-
且只能发生一次
2. 上层封装
| 工具 | 作用 |
|---|---|
std::promise |
手动写结果 |
std::future |
读结果 |
std::shared_future |
多消费者读同一结果 |
std::packaged_task |
把"函数调用"绑到 future |
std::async |
一步完成"起任务 + 给 future" |
3. 适用
-
后台计算返回结果
-
线程启动结果 / 错误信息回传
-
一次性请求-响应
不适合:
-
持续消息流
-
高频事件
-
多生产者多消费者管道
六、信号量(C++20):配额与门控
1. 语义
std::counting_semaphore<> sem(3); // 最多 3 个线程进入
void worker() {
sem.acquire();
do_work();
sem.release();
}
-
acquire:计数 > 0 则减一,否则阻塞 -
release:计数加一,唤醒等待者 -
std::binary_semaphore是计数上限为 1 的特例
2. 和 mutex 的区别
| 维度 | mutex | semaphore |
|---|---|---|
| 所有者 | 有:谁锁谁解 | 无:任何人可 release |
| 用途 | 保护临界区 | 控制并发数 / 资源配额 |
| 是否传数据 | 否 | 否 |
3. 适用
-
连接池
-
线程数限流
-
生产者消费者缓冲区槽位
-
替代某些 condvar + 计数器的手写模型
七、Latch / Barrier(C++20):阶段协调
1. std::latch:一次性倒计时
std::latch lat(N);
// N 个线程各自:
do_work();
lat.count_down();
// 某线程:
lat.wait(); // 等所有人完成
适合:
-
任务拆分后等全部完成
-
初始化阶段屏障
2. std::barrier:可重用阶段屏障
std::barrier bar(N, []{
// 每轮结束时的汇合动作
});
while (running) {
phase_work();
bar.arrive_and_wait();
}
适合:
-
迭代算法
-
并行 for 的分阶段同步
-
仿真、渲染、数值计算
八、消息传递:把"共享"变成"传递"
这是工业级并发里最重要的范式转变:
不要让大家抢同一块内存,而是把数据所有权从一个线程移到另一个线程。
1. 线程安全队列
template<class T>
class ThreadSafeQueue {
std::queue<T> q;
std::mutex m;
std::condition_variable cv;
public:
void push(T v) {
{
std::lock_guard<std::mutex> lk(m);
q.push(std::move(v));
}
cv.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [this]{ return !q.empty(); });
T v = std::move(q.front());
q.pop();
return v;
}
};
通信语义:
-
生产者拥有数据 → push → 所有权转移
-
消费者 pop → 独占数据
-
没有共享可变状态被并发访问
2. 无锁队列 / SPSC 通道
-
单生产者单消费者可用 ring buffer + atomic
-
MPMC 可用 CAS、epoch、hazard pointer
-
游戏、交易系统、网络栈常用
3. 为什么更高级
-
死锁面更小
-
可测试性更好
-
线程边界清晰
-
容易扩展到线程池 / actor 模型
九、停止与取消:C++20 的 jthread/ stop_token
线程通信不只是"传数据",还包括"让你停下"。
void worker(std::stop_token st) {
while (!st.stop_requested()) {
do_periodic();
std::this_thread::sleep_for(10ms);
}
}
std::jthread t(worker);
// 析构时自动 request_stop() + join()
相关原语:
-
std::jthread -
std::stop_token -
std::stop_source -
std::stop_callback
这是"控制面通信",和"数据面通信"同样重要。
十、线程局部存储:用"不共享"来通信
thread_local int cache = 0;
这不是线程间传数据,而是反方向:
通过避免共享,消除同步成本。
常见用法:
-
每线程缓存
-
每线程随机数生成器
-
每线程日志缓冲
-
后续汇总(reduce)时才通信
十一、高级 / 系统级通信
1. 无锁结构
-
lock-free stack / queue
-
RCU
-
hazard pointer
-
atomic smart pointer(
std::atomic<std::shared_ptr>C++20)
适合:极低延迟、高争用、内核/交易/网络栈。
2. 协程管道(C++20 起)
task<int> producer();
task<void> consumer() {
int x = co_await producer();
}
协程本身不是线程,但和线程模型结合后,可以形成:
-
线程池 + 协程任务
-
async / await 风格管道
-
高并发 I/O 通信
3. 共享内存 + 外部 IPC
同一进程内是线程通信;跨进程常借助:
-
mmap/shm -
管道 / socket
-
消息队列
但那些已经超出"C++ 标准线程库"范围。
十二、选型地图
| 你想做的事 | 首选 |
|---|---|
| 保护多字段状态 | mutex+ RAII 锁 |
| 等"数据就绪" | condition_variable |
| 简单标志 / 计数 | std::atomic |
| 一次性返回结果 | future/ promise |
| 限制并发数 | counting_semaphore |
| 等所有任务完成 | latch |
| 每轮迭代汇合 | barrier |
| 生产者消费者 | 线程安全队列 |
| 低延迟无锁 | atomic + CAS |
| 让线程优雅停 | jthread+ stop_token |
| 避免共享 | thread_local/ 消息传递 |
十三、核心结论
C++ 多线程通信不是一堆孤立 API,而是一张谱系:
-
最底层:原子、内存序、mutex ------ 解决可见性与互斥
-
事件层:condvar、semaphore、latch、barrier ------ 解决"什么时候继续"
-
任务层:promise / future / packaged_task / async ------ 解决"结果去哪"
-
架构层:消息队列、线程池、无锁结构、协程 ------ 解决"系统怎么组织"
新手学 mutex,进阶学 condvar,老手写消息传递,架构师尽量少共享。