C++ 多线程通信方式全景:从共享内存到结构化协调

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. 三个铁律

  1. wait必须有谓词,否则丢唤醒 / 假唤醒都会出错

  2. notify前通常先修改受保护状态

  3. 条件变量不携带数据,只携带"状态可能变了"的信号

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,而是一张谱系:

  1. 最底层:原子、内存序、mutex ------ 解决可见性与互斥

  2. 事件层:condvar、semaphore、latch、barrier ------ 解决"什么时候继续"

  3. 任务层:promise / future / packaged_task / async ------ 解决"结果去哪"

  4. 架构层:消息队列、线程池、无锁结构、协程 ------ 解决"系统怎么组织"

新手学 mutex,进阶学 condvar,老手写消息传递,架构师尽量少共享。

相关推荐
weixin199701080161 小时前
《1688消息服务落地方案:alibaba.message.* 与 Webhook 回调的可靠性与重试策略》(附Python源码)
开发语言·python
ZHOUPUYU1 小时前
PHP 9.0 前瞻:下一代 PHP 会带来哪些新功能?
android·开发语言·php
某不知名網友1 小时前
ROS2 核心通信:话题发布与订阅详解
开发语言·c++
C++ 老炮儿的技术栈1 小时前
char (*csConnectName)[256]; 和 char csConnectName [256] 有什么区别
java·c语言·开发语言·c++·人工智能·算法·c
Nebula_g1 小时前
JavaSE加强:线程Thread(线程安全重点)
java·开发语言·jvm·算法·javase·技术栈
小灰灰搞电子1 小时前
Rust+Slint 实现一个终端软件源码分享(半成品)
开发语言·rust
大鹏说大话1 小时前
C++ STL 容器怎么选?vector、list、map、unordered_map 对比
开发语言·c++·list
JMchen2 小时前
性能优化——让Native代码飞起来
android·c++·性能优化
宇卿.2 小时前
C#语句:
开发语言·c#