数据不丢失需主动设计,源于非原子操作与缺乏同步;应使用std::mutex配合std::lock_guard保护共享状态,避免手动加解锁遗漏异常路径。

数据不丢失在多线程中不是默认行为,而是必须主动设计的结果。它通常指共享变量的更新不被覆盖、日志不被截断、队列元素不被吞掉——这些现象背后几乎都指向同一类问题:非原子操作 + 缺乏同步。
std::mutex + std::lock_guard 是最稳的起点
对任意可变共享状态(如 int counter、std::vector<t></t>、std::ofstream),只要多个线程会写入或读写混用,就必须加锁保护临界区。手写 mtx.lock()/mtx.unlock() 容易漏掉异常路径下的解锁,所以优先用 std::lock_guard:
std::mutex mtx;
int shared_counter = 0;
void thread_worker() {
for (int i = 0; i guard(mtx); // 构造即加锁,析构即释放
++shared_counter; // 这一行现在是原子语义的“整体”
}
}
- 不要在锁内做耗时操作(比如网络请求、大块文件写入),否则其他线程会长时间阻塞
- 锁的作用域越小越好;如果需要多次访问共享对象,考虑一次性拷贝出只读副本再处理
-
std::lock_guard不支持手动解锁或转移所有权,够用就别换std::unique_lock
std::atomic 适合简单类型且无依赖的操作
当共享变量只是基础类型(int、bool、指针),且操作满足“读-改-写”可被硬件原子指令覆盖(如 ++、fetch_add),std::atomic 是零开销替代方案:
std::atomic_int counter{0};
void thread_worker() {
for (int i = 0; i
-
std::atomic不能用于结构体或容器;哪怕std::atomic<:string></:string>也非法 - 不要误以为
std::atomic能解决“逻辑原子性”问题——例如“检查值是否为0再设为1”,仍需锁或compare_exchange_weak - 内存序(
memory_order)选错会导致看似正常但偶发失败;日常用默认(seq_cst)最安全
日志写入不丢行的关键是缓冲与锁粒度平衡
多线程往同一个文件或终端打日志时,常见“半行混杂”或整条丢失,本质是 std::ofstream 非原子,且底层 <code>write() 系统调用可能被中断。解决方案不是锁整个日志函数,而是:
- 每条日志消息先格式化成完整字符串(避免在锁内拼接)
- 用一个
std::mutex保护最终的log_file 和 <code>log_file.flush() - 禁用
std::ios_base::sync_with_stdio(false)(否则 C++ 流和 C stdio 缓冲不同步,可能丢) - 若性能吃紧,改用无锁环形缓冲 + 单独日志线程消费,但复杂度陡增
队列/链表等容器必须封装锁,不能只锁部分操作
像 std::queue 或自定义链表,即使只用 push() 和 pop(),也不能假设“各自加锁就安全”。典型错误:
// ❌ 错误:size() 和 pop() 之间可能被其他线程修改
if (!q.empty()) {
auto x = q.front(); // 读
q.pop(); // 写 —— 两步不连续!
}
// ✅ 正确:整个判断+取值+删除必须在一把锁下完成
std::lock_guard<:mutex> guard(mtx);
if (!q.empty()) {
auto x = q.front();
q.pop();
}
</:mutex>
- 所有对外暴露的成员函数(
empty()、size()、front()、pop())都应内部加锁,或提供带锁的原子接口(如try_pop(T& out)) - 返回引用或指针的接口(如
front())在并发下极危险,尽量改为值返回 - STL 容器本身不保证线程安全——哪怕只读,若另一线程正在写,仍是未定义行为
真正难的不是加锁,而是判断哪里该加、加多大范围、以及锁住的资源是否还隐含其他依赖(比如日志文件句柄被 fclose、队列节点内存被提前 delete)。这些边界稍有遗漏,数据就悄无声息地丢了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











