Data Race(数据竞争) 和 Race Condition(竞态条件) 经常被混淆,但在严格的并发编程语义下,它们并不是同一个概念,不能画等号
简单来说:
- Data Race(数据竞争) 是一个关于内存访问的底层物理问题
- Race Condition(竞态条件) 是一个关于执行顺序与业务逻辑的高层逻辑问题
1. 什么是 Data Race(数据竞争)#
在 C++ 标准中,Data Race 有着极其严格的定义。当满足以下三个条件时,就一定会发生数据竞争:
- 两个或多个线程同时(并发)访问同一个内存地址
- 至少有一个访问是写操作(Write)
- 这些访问没有使用任何同步机制(比如没有加互斥锁
std::mutex,也没有使用原子变量std::atomic)
后果:未定义行为#
在 C++ 规范里,只要代码中存在 Data Race,整个程序就会直接沦为未定义行为(UB)。 编译器可能会把代码优化得面目全非,导致不可预知的崩溃或极其诡异的 Bug
2. 什么是 Race Condition(竞态条件)#
Race Condition 指的是程序的输出依赖于多个线程执行的相对顺序(时序)。如果由于调度的随机性,导致线程执行顺序不对,程序的逻辑就会出错。
它通常发生在业务层。即使内存访问是安全的(比如每一步都加了锁,不会发生 Data Race),但只要多个操作之间的顺序出了错,依然会触发 Race Condition
3. 对比:有 Race Condition 但没有 Data Race#
假设我们用一个线程安全的并发链表(所有操作都加了锁)来写一段多线程代码:
1// 假设这个并发链表内部是互斥锁保护的,没有 Data Race
2ThreadSafeList<int> myList;
3
4// 线程 A 和 线程 B 都在执行这段代码
5if (!myList.contains(42)) {
6 // 隐患:在检查和插入之间,当前线程可能会被挂起
7 myList.insert(42);
8}
为什么它没有 Data Race#
因为 contains 和 insert 内部都正确地加了锁,多线程并发调用它们时,内存访问是完全安全的,绝不会触发 C++ 的未定义行为
为什么它有 Race Condition(竞态条件)#
假设链表初始没有 42:
- 线程 A 检查
contains(42),返回false - 此时时间片到了,线程 A 被挂起,线程 B 切进来
- 线程 B 检查
contains(42),返回false - 线程 B 执行
insert(42) - 线程 A 被唤醒,继续执行
insert(42)
结果: 链表里被重复插入了两个 42。由于执行时序的先后破坏了“不包含才插入”的业务逻辑,这就是 Race Condition
4. 总结与直观关系#
- 有 Data Race,无 Race Condition: 很少见,比如两个线程在没有同步的情况下同时往一个全局变量写同一个固定的值(如
x = 42)。无论谁先写,结果都是42,业务逻辑没错,但由于没加锁,它依然是 Data Race(C++ 判定为 UB)。 - 有 Race Condition,无 Data Race: 上文提到的并发链表例子。每一步都安全加锁,但宏观时序混乱导致逻辑错误。
- 两者都有(最常见): 两个线程在没有锁保护的情况下,同时对一个普通的
int count变量执行count++。这既破坏了内存访问安全(Data Race),又导致最终计数不对(Race Condition)。