C++实现线程同步的四种方式总结

前言


多线程编程的核心难点不在「怎么开线程」,而在「多个线程同时访问同一份数据时,如何保证结果可预期」。C++ 标准把没有同步保护的并发读写称为数据竞争(data race),而数据竞争是未定义行为(undefined behavior, UB)——不是「结果偶尔不对」,而是编译器可以假设它不存在,从而做出任意优化。


一个常见误解是「只要不加锁就不会死锁,所以无锁最好」。实际上无锁代码要自己处理内存序、ABA 问题和对象生命周期,写错的概率远高于加锁。另一个误解是把volatile当成同步手段,它既不做原子性保证也不建立线程间的可见性顺序,只能用于内存映射 I/O 等特殊场景。


本文按「互斥量、条件变量、原子操作、读写锁」四类梳理 C++ 的线程同步手段,说明各自保证什么、不保证什么,并给出可以直接对照的示例。文中代码以 C++17 为基准,涉及更新的标准会单独标注。


一、同步的本质:建立 happens-before


线程同步要解决两件事:互斥(同一时刻只有一个线程进入临界区)和可见性(一个线程的写入能被另一个线程看到)。C++ 内存模型用 happens-before 关系描述后者:如果写操作 A happens-before 读操作 B,那么 B 一定能看到 A 的结果。


互斥量通过 acquire/release 语义建立这种关系:加锁是 acquire,解锁是 release,同一互斥量上的 unlock synchronizes-with 后续的 lock。原子操作则通过内存序参数控制同样的事情。理解这一点,才能明白为什么「两个线程各写各的变量」也需要同步——只要它们共享缓存行或存在逻辑依赖,缺少 happens-before 就可能读到旧值。




方式头文件解决的问题典型开销主要限制



std::mutex<mutex>临界区互斥无竞争时接近原子指令,有竞争时可能休眠不可递归,不可拷贝

std::condition_variable<condition_variable>等待某个条件成立等待时释放锁并休眠必须搭配互斥量与谓词

std::atomic<atomic>单个对象的原子读改写通常是一条或几条原子指令只保护它自己,不保护关联数据

std::shared_mutex<shared_mutex>读多写少场景的读写分离比普通互斥量重写者可能饥饿,C++17 起提供



二、互斥量:最通用的一把锁


std::mutex提供lock()、unlock()、try_lock()。手写 lock/unlock 在异常路径上极易漏掉 unlock,因此应当用 RAII 包装器:



  • std::lock_guard<std::mutex>:构造时加锁,析构时解锁,不能提前释放,最轻量。

  • std::unique_lock<std::mutex>:可延迟加锁、可提前unlock()、可转移所有权,条件变量必须用它。

  • std::scoped_lock(C++17):可一次锁多个互斥量,内部用死锁避免算法,替代std::lock加多个lock_guard的写法。


// 适用于 C++17
#include <mutex>

class Counter {
public:
void add(int n) {
std::lock_guard<std::mutex> lk(m_);
value_ += n;
}
int get() const {
std::lock_guard<std::mutex> lk(m_);
return value_;
}
private:
mutable std::mutex m_;
int value_ = 0;
};

这里m_声明为mutable,是因为get()是 const 成员函数但仍需加锁。这不是风格问题:如果 const 函数不加锁就读value_,它与并发的add()构成数据竞争。


std::recursive_mutex允许同一线程重复加锁,但它常被用来掩盖「锁的职责不清」的设计问题。能用普通互斥量把临界区规划清楚时,不要用递归锁。


三、条件变量:等待状态变化


条件变量解决的是「线程要等到某个条件成立才继续」。它的正确用法有三条硬性要求:等待前必须持有互斥量;必须用谓词形式等待;修改条件后要通知。


// 适用于 C++17
#include <condition_variable>
#include <mutex>
#include <queue>
#include <utility>

template <typename T>
class BlockingQueue {
public:
void push(T value) {
{
std::lock_guard<std::mutex> lk(m_);
q_.push(std::move(value));
}
cv_.notify_one(); // 通知放在解锁之后,减少被唤醒线程的等待
}

bool pop(T& out) {
std::unique_lock<std::mutex> lk(m_);
cv_.wait(lk, [this] { return !q_.empty() || closed_; });
if (q_.empty()) return false; // 已关闭且队列为空
out = std::move(q_.front());
q_.pop();
return true;
}

void close() {
{
std::lock_guard<std::mutex> lk(m_);
closed_ = true;
}
cv_.notify_all();
}

private:
std::mutex m_;
std::condition_variable cv_;
std::queue<T> q_;
bool closed_ = false;
};

cv_.wait(lk, pred)的语义是:在持有锁的状态下求值pred,为假则原子地释放锁并阻塞;被唤醒后重新加锁并再次求值。谓词解决了两个问题——虚假唤醒(spurious wakeup,等待可能无故返回)和丢失唤醒(通知发生在 wait 之前)。绝不能用裸wait(lk)配if,因为被唤醒时条件未必已经成立。


notify_one与notify_all的选择取决于等待者是否都在等同一个条件:消费者队列用notify_one即可,而「关闭」「广播配置变更」必须用notify_all,否则部分线程永远醒不过来。


四、原子操作与读写锁


std::atomic<T>让对单个对象的读、写、读改写成为不可分割的操作。最常用的成员是load、store、fetch_add、exchange和compare_exchange_weak/strong。


// 适用于 C++17
#include <atomic>
#include <thread>
#include <vector>

std::atomic<long long> hits{0};

void worker(int rounds) {
for (int i = 0; i < rounds; ++i) {
hits.fetch_add(1, std::memory_order_relaxed);
}
}

int main() {
std::vector<std::thread> pool;
for (int i = 0; i < 4; ++i) pool.emplace_back(worker, 100000);
for (auto& t : pool) t.join();
return hits.load() == 400000 ? 0 : 1;
}

memory_order_relaxed只保证这个计数器自身的原子性,不建立与其他变量的顺序关系。这里只需要计数,所以够用;一旦要用计数器去「发布」另一块数据(例如标志某块缓冲区已写好),就必须用release/acquire配对,或者干脆用默认的seq_cst。


原子类型不能替代互斥量的场景很明确:需要同时修改两个及以上相互关联的变量时,单个atomic无法保证这组修改的整体一致性。std::atomic<T>对非平凡类型(比如较大的结构体)也不保证无锁,is_lock_free()的结果依赖实现。


读写锁:读多写少的分工


std::shared_mutex(C++17)提供两种所有权:独占(写)和共享(读)。用std::unique_lock<std::shared_mutex>获取写锁,用std::shared_lock<std::shared_mutex>获取读锁。多个读者可以同时进入,写者必须独占。


// 适用于 C++17
#include <map>
#include <shared_mutex>
#include <string>
#include <utility>

class Config {
public:
std::string get(const std::string& key) const {
std::shared_lock<std::shared_mutex> lk(m_);
auto it = data_.find(key);
return it == data_.end() ? std::string{} : it->second;
}

void set(std::string key, std::string value) {
std::unique_lock<std::shared_mutex> lk(m_);
data_[std::move(key)] = std::move(value);
}

private:
mutable std::shared_mutex m_;
std::map<std::string, std::string> data_;
};

读写锁并非「一定比互斥量快」。它的内部状态更复杂,短临界区下额外开销可能抵消并发收益;而且不同标准库实现的公平策略不同,读请求持续不断时写者可能长期得不到锁(写者饥饿)。是否值得,要在自己的负载下用std::chrono::steady_clock测量,而不是凭直觉。


C++20 还提供了std::counting_semaphore和std::binary_semaphore(需要 C++20 及以上),适合「限制同时访问某资源的线程数」这类计数型同步;它们不提供所有权概念,使用时必须自己保证配对。


常见坑点



  1. ❌ 用volatile标志位做线程间通知,认为它「禁止优化所以能同步」。✅ 用std::atomic<bool>配acquire/release,或直接用互斥量;volatile不建立 happens-before。

  2. ❌ 只给写操作加锁,读操作直接读共享变量,认为「读不会出问题」。✅ 读也要加锁或用原子类型;并发的读与写同样构成数据竞争。

  3. ❌cv.wait(lk)外面套if (q.empty()),被虚假唤醒后直接访问空队列。✅ 用cv.wait(lk, pred)或while (!pred()) cv.wait(lk);。

  4. ❌ 先notify_one()再解锁,且通知的对象依赖锁内状态。✅ 一般应在修改完共享状态并解锁后再通知;至少在持有锁时修改状态,通知位置按语义选择。

  5. ❌ 用两个互斥量时各自lock(),两个线程加锁顺序相反导致死锁。✅ 用 C++17 的std::scoped_lock lk(m1, m2);一次获取。

  6. ❌ 把一个std::atomic<int>当作「整个结构体的锁」,同时读写结构体其他字段。✅ 原子类型只保护自己;复合状态用互斥量整体保护。

  7. ❌ 在持锁期间调用未知的回调、写日志或做阻塞 I/O。✅ 在锁内只做数据修改,把耗时操作移到锁外,缩小临界区。

  8. ❌std::thread对象析构前既不join()也不detach()。✅ 确保每条路径都汇合或分离,否则析构时调用std::terminate。


总结




同步手段保证不保证适用场景



std::mutex临界区互斥、unlock 与后续 lock 的可见性不防死锁、不可重入绝大多数共享状态保护

std::condition_variable等待/通知配对,配合谓词防虚假唤醒不存储通知,不替代互斥量生产者消费者、等待状态变化

std::atomic单个对象的原子性与指定内存序不保护其他变量,不保证无锁计数器、标志位、无锁结构的基础件

std::shared_mutex多读单写的并发控制不保证公平,不保证更快读远多于写的配置、缓存



四种方式不是互斥的选择题,而是一套工具箱:互斥量是默认答案,条件变量用来等待,原子类型用于单点计数与标志,读写锁用于读多写少且临界区不太短的情况。真正决定程序正确性的,是有没有为每一份共享数据明确「谁在什么时候可以访问」,而不是用了哪种锁。




相关新闻

通用Agent架构设计:大脑+技能+护栏,重构软件意图驱动新范式

通用Agent架构设计:大脑+技能+护栏,重构软件意图驱动新范式

1. 从“功能堆叠”到“意图驱动”&#xff1a;通用Agent到底在改什么过去十几年&#xff0c;我们做软件的方式基本没变过&#xff1a;产品经理画页面&#xff0c;后端定义接口&#xff0c;前端拼组件&#xff0c;测试写用例&#xff0c;最后打包发版。用户打开一个App&#xff…

2026/10/20 22:19:42 阅读更多 →
JMeter分布式压测Docker化实践:从原理到排坑

JMeter分布式压测Docker化实践:从原理到排坑

第一次在一次大促前的性能摸底中&#xff0c;我把JMeter分布式压测环境从一台台物理机迁到了Docker上&#xff0c;原以为只是把启动命令换成容器方式执行&#xff0c;结果光RMI连接就排查了一个下午。稳定跑起来之后我才确信&#xff0c;这个方向选对了——它解决了我过去两年在…

2026/10/20 22:19:42 阅读更多 →
知识蒸馏进阶:变分信息蒸馏VID原理与实战

知识蒸馏进阶:变分信息蒸馏VID原理与实战

1. 从知识蒸馏的瓶颈说起如果你这两年一直在跟进模型压缩和知识蒸馏方向&#xff0c;大概率会有一种感觉&#xff1a;传统KD&#xff08;Knowledge Distillation&#xff09;那套“软标签KL散度”的玩法&#xff0c;在早期分类任务上确实好使&#xff0c;但一旦碰到特征维度高、…

2026/10/20 22:18:29 阅读更多 →

最新新闻

通用型Makefile:多级目录自动收集与依赖追踪实战

通用型Makefile:多级目录自动收集与依赖追踪实战

做C/C项目时间长了&#xff0c;几乎每个人都会攒一套自己的Makefile。早年我在一个跨平台工具项目里用的还是那种把所有文件路径一行行写死的Makefile&#xff0c;源码目录从两层涨到四层之后&#xff0c;每次新增一个模块都要回去改源文件列表、改编译规则、改clean&#xff0…

2026/10/20 23:02:12 阅读更多 →
STM32启动流程全解析:从复位向量到main函数前的完整路径

STM32启动流程全解析:从复位向量到main函数前的完整路径

朋友们平时点灯、调外设都很顺&#xff0c;但有没有想过这样一个问题&#xff1a;你在Keil里点一下下载&#xff0c;程序跑到main之前&#xff0c;芯片到底做了什么&#xff1f;很多新手甚至部分老手&#xff0c;遇到程序不跑、变量莫名清零、一进中断就HardFault这类问题&…

2026/10/20 23:02:12 阅读更多 →
diagram-design:从草图到可维护工程资产的图表设计化生产方法论

diagram-design:从草图到可维护工程资产的图表设计化生产方法论

1. 从一张草图到一套系统&#xff1a;diagram-design 到底在解决什么问题 第一次听到 “diagram-design” 这个词&#xff0c;很多人会下意识觉得它只是“画图”的另一种说法。但真正在项目里被图表折磨过的人知道&#xff0c;画图本身从来不是最痛的部分。最痛的是&#xff1a…

2026/10/20 23:02:12 阅读更多 →
RNN文本分类实战:从数据预处理到BiGRU模型调参全记录

RNN文本分类实战:从数据预处理到BiGRU模型调参全记录

前阵子接手一个文本分类需求&#xff1a;给一批用户评论打标签&#xff0c;正向、负向、中性三选一。同事的第一反应是直接上预训练模型&#xff0c;说效果肯定好。我的想法不太一样——先别急着烧显卡&#xff0c;RNN文本分类这个经典方案&#xff0c;在数据量只有几万条、以短…

2026/10/20 23:02:12 阅读更多 →
从JSESSIONID泄露到任意文件上传:Web安全链路复盘与加固实践

从JSESSIONID泄露到任意文件上传:Web安全链路复盘与加固实践

先聊一个我实际遇到的场景。某集团IT负责人打来电话时&#xff0c;语气已经明显不对&#xff1a;服务器上多了一个陌生文件&#xff0c;文件名看起来是正常图片&#xff0c;后缀却是JSP。顺着访问日志往回翻&#xff0c;攻击行为大概发生在十几个小时前&#xff0c;先有几个来自…

2026/10/20 23:02:12 阅读更多 →
运输层深度解析:TCP可靠性、拥塞控制与排障实战

运输层深度解析:TCP可靠性、拥塞控制与排障实战

刚接触网络那几年&#xff0c;我最早排的一个“玄学故障”就是TCP连接建不起来。两台服务器明明在同一个二层网段&#xff0c;ping 100%通&#xff0c;HTTP就是打不开。抓包一看&#xff0c;三次握手的最后一个ACK发出去就没了下文。那时候我才真正理解&#xff0c;运输层虽然不…

2026/10/20 23:01:09 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →