你想了解的是 C++ 中的竞态条件(Race Condition),它是多线程编程中最常见也最容易出问题的场景之一,核心是多线程对共享资源的无序访问导致程序行为不可预测。
一、竞态条件的通俗解释
先用生活中的例子帮你理解:假设你和朋友同时去抢桌子上最后一瓶可乐,你伸手去拿的同时朋友也伸手,最终谁拿到、会不会把瓶子碰倒,都是不确定的 —— 这就是 “竞态” 的本质:多个 “执行者”(线程)同时争抢同一个 “资源”(变量 / 内存 / 设备等),且执行顺序不受控制,导致最终结果不可预测。
在 C++ 中,竞态条件特指:
二、C++ 竞态条件的代码示例
下面是一个典型的竞态场景:两个线程同时对同一个全局变量做累加,预期结果是 20000,但实际运行结果几乎每次都不一样(比如 15678、18901 等)。
#include <iostream>
#include <thread>
#include <vector>
// 共享资源:全局变量
int counter = 0;
// 累加函数(每个线程执行10000次累加)
void increment() {
for (int i = 0; i < 10000; ++i) {
// 这里的counter++不是原子操作!
// 它拆解为3步:读取counter值 → 加1 → 写回counter
// 多线程下这3步会被打乱执行,导致数据覆盖
counter++;
}
}
int main() {
// 创建两个线程
std::thread t1(increment);
std::thread t2(increment);
// 等待线程执行完毕
t1.join();
t2.join();
// 预期输出20000,但实际结果随机
std::cout << "最终counter值:" << counter << std::endl;
return 0;
}
为什么结果不对?
counter++ 看似是一行代码,实则是非原子操作(不能一次性完成),拆解为:
当两个线程同时执行这三步时,可能出现这样的混乱:
- 线程 1 读取 counter=5 → 线程 2 也读取 counter=5;
- 线程 1 加 1 得到 6 → 线程 2 加 1 也得到 6;
- 线程 1 写回 6 → 线程 2 也写回 6;原本应该累加 2 次(5→6→7),结果只累加了 1 次(5→6),最终结果就会偏小。
三、C++ 中如何避免竞态条件?
核心思路是保证共享资源的访问是 “互斥” 的(同一时间只有一个线程能修改),常用方案:
加互斥锁的解决原理:「独占临界区,让三步整体串行」
互斥锁的核心思路是给「读→加→写」这一整套操作加 “排他权”:同一时间只允许一个线程进入包含这三步的代码块(临界区),让这三步作为一个 “整体” 被完整执行,其他线程必须等待当前线程执行完所有三步并解锁后,才能进入临界区执行。
基于「读→加→写」三步的执行流程(加锁后):
假设初始 counter=5,两个线程执行加锁后的 counter++:
- 读取 counter=5 到寄存器;
- 寄存器值加 1 → 6;
- 写回 counter=6;
- 读取 counter=6 到寄存器;
- 寄存器值加 1 → 7;
- 写回 counter=7;
核心本质:互斥锁没有改变「读→加→写」是三步操作的事实,而是通过强制线程串行执行这三步,杜绝了多个线程同时执行这三步的可能,从 “执行顺序” 上避免了指令交叉。
对应代码的关键逻辑:
std::lock_guard<std::mutex> lock(mtx); // 加锁,独占临界区
counter++; // 这三步作为整体被串行执行
// 解锁,其他线程才能执行counter++
原子操作的解决原理:「硬件级合并三步,让操作不可打断」
原子操作的核心思路是利用 CPU 的硬件指令,把「读→加→写」三步合并成一个不可拆分的原子指令——CPU 执行这个原子指令时,不会被其他线程的指令插入,相当于把三步变成了 “一步到位” 的操作,从 “操作本身” 上消除了被打断的可能。
基于「读→加→写」三步的执行流程(原子操作后):
同样初始 counter=5,两个线程执行原子版 counter++(如 counter.fetch_add(1)):
- CPU 接收到原子累加指令,硬件层面锁定 counter 对应的内存地址;
- 一次性完成:读取 counter=5 → 加 1 → 写回 counter=6;
- 硬件解锁内存地址,整个过程无任何中断点,线程 2 无法在这期间访问 counter;
- 此时 counter 已为 6,CPU 同样一次性完成 “读→加→写”,最终得到 7;
核心本质:原子操作直接改变了「读→加→写」的执行形态 —— 从软件层面的三步非原子操作,变成了硬件层面的一步原子操作,根本不存在 “被其他线程打断” 的中间状态。
对应代码的关键逻辑:
std::atomic<int> counter(0);
counter.fetch_add(1); // 硬件级原子指令,三步合并为一步,不可打断,其他线程自旋
线程1在对counter执行原子操作fetch_add()时会锁定counter, 然后其他线程尝试对counter操作时就会自旋等待;
核心原理对比(紧扣「读→加→写」三步)
| 对「读→加→写」的处理 | 保留三步操作,但强制串行执行这三步 | 硬件层面合并三步为一步,变成不可拆分的原子指令 |
| 阻断竞态的方式 | 从 “执行顺序” 上杜绝多线程同时执行三步 | 从 “操作本身” 上消除被打断的可能 |
| 依赖的层面 | 操作系统内核态(线程阻塞 / 唤醒、上下文切换) | CPU 硬件指令(用户态,无内核介入) |
| 中间状态是否存在 | 存在(三步仍有中间态,但其他线程看不到) | 不存在(无任何中间态,一步完成) |
代码演示:
互斥量: 修复后的代码(用 std::mutex):
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex mtx; // 互斥锁
void increment() {
for (int i = 0; i < 10000; ++i) {
// 加锁:同一时间只有一个线程能进入这个代码块
std::lock_guard<std::mutex> lock(mtx);
counter++;
// 解锁:lock_guard析构时自动解锁,避免忘记解锁导致死锁
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "最终counter值:" << counter << std::endl; // 稳定输出20000
return 0;
}
原子操作:适合单变量的简单操作
原子操作专门解决 “单个变量的非原子操作问题”,比如之前的累加场景,用原子操作更高效:
#include <iostream>
#include <thread>
#include <atomic>
// 原子变量:所有操作都是不可打断的
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 10000; ++i) {
// fetch_add:原子累加(读取+加1+写回一步完成)
counter.fetch_add(1);
// 等价于 counter++(原子版)
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
// 稳定输出20000,且性能比互斥锁更高
std::cout << "最终counter值:" << counter << std::endl;
return 0;
}
互斥锁:适合多步操作的临界区
如果逻辑是 “先判断变量值,再修改,再记录日志” 这类多步联动操作,原子操作无法覆盖,必须用互斥锁:
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
int balance = 1000; // 账户余额(共享资源)
std::mutex mtx;
std::vector<std::string> logs; // 操作日志(共享资源)
// 取款函数:多步操作(判断余额→扣减→记录日志)
void withdraw(int amount) {
// 加锁:保护整个临界区(多步操作)
std::lock_guard<std::mutex> lock(mtx);
// 第一步:判断余额是否足够
if (balance >= amount) {
// 第二步:扣减余额
balance -= amount;
// 第三步:记录日志
logs.push_back("取款" + std::to_string(amount) + "元,余额剩余" + std::to_string(balance) + "元");
} else {
logs.push_back("取款" + std::to_string(amount) + "元失败,余额不足");
}
}
int main() {
// 两个线程同时取款
std::thread t1(withdraw, 800);
std::thread t2(withdraw, 500);
t1.join();
t2.join();
// 输出日志(结果可预测)
for (const auto& log : logs) {
std::cout << log << std::endl;
}
// 最终余额:200元(正确结果)
std::cout << "最终账户余额:" << balance << std::endl;
return 0;
}
网硕互联帮助中心


![打卡信奥刷题(2806)用C++实现信奥题 P4084 [USACO17DEC] Barn Painting G-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/02/20260207054926-6986d26629a91-220x150.png)

评论前必须登录!
注册