为什么跨线程更改共享变量的代码显然没有受到竞争条件的影响
Why does code mutating a shared variable across threads apparently NOT suffer from a race condition?
我正在使用Cygwin GCC并运行以下代码:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
unsigned u = 0;
void foo()
{
u++;
}
int main()
{
vector<thread> threads;
for(int i = 0; i < 1000; i++) {
threads.push_back (thread (foo));
}
for (auto& t : threads) t.join();
cout << u << endl;
return 0;
}
用以下行编译:g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o
。
它打印1000,这是正确的。然而,由于线程覆盖了以前增加的值,我预计会有一个较小的数字。为什么这个代码不受相互访问的影响?
我的测试机器有4个核心,我对我所知道的程序没有任何限制。
当用更复杂的东西(例如)替换共享foo
的内容时,问题仍然存在
if (u % 3 == 0) {
u += 4;
} else {
u -= 1;
}
foo()
非常短,每个线程可能在下一个线程生成之前就完成了。如果在u++
之前在foo()
中随机添加一个睡眠时间,您可能会开始看到您的期望。
重要的是要理解竞争条件并不能保证代码会错误运行,只是它可以做任何事情,因为这是一种未定义的行为。包括按预期运行。
特别是在X86和AMD64机器上,竞争条件在某些情况下很少会导致问题,因为许多指令都是原子指令,并且一致性保证非常高。在多处理器系统上,这些保证在一定程度上减少了,因为许多指令都需要锁前缀作为原子。
如果在您的机器上增量是一个原子操作,即使根据语言标准它是Undefined Behavior,它也可能正确运行。
特别是,我预计在这种情况下,代码可能会被编译为原子Fetch and Add指令(X86程序集中的Add或XADD),这在单处理器系统中确实是原子指令,但在多处理器系统上,这并不能保证是原子指令并且需要锁才能实现。如果您在多处理器系统上运行,则会有一个窗口,线程可能会在其中干扰并产生不正确的结果。
具体来说,我使用https://godbolt.org/foo()
编译为:
foo():
add DWORD PTR u[rip], 1
ret
这意味着它只执行加法指令,对于单个处理器来说,加法指令将是原子指令(尽管如上所述,对于多处理器系统来说并非如此)。
我认为,如果你在u++
之前或之后睡一觉,那就不算什么了。相反,操作u++
转换成的代码——与调用foo
的生成线程的开销相比——执行得非常快,因此不太可能被拦截。然而,如果你"延长"操作u++
,那么比赛条件将变得更有可能:
void foo()
{
unsigned i = u;
for (int s=0;s<10000;s++);
u = i+1;
}
结果:694
BTW:我也试过
if (u % 2) {
u += 2;
} else {
u -= 1;
}
它给我的时间大多是1997
,但有时是1995
。
它确实受到种族条件的影响。在foo
中将usleep(1000);
放在u++;
之前,我每次看到不同的输出(<1000)。
-
尽管竞争条件确实存在,但为什么它没有为您显现,可能的答案是
foo()
与启动线程所需的时间相比非常快,每个线程甚至在下一个线程开始之前就完成了。但是 -
即使是你的原始版本,结果也因系统而异:我在(四核)Macbook上按你的方式尝试了一下,在十次运行中,我三次获得1000,六次获得999,一次获得998。因此,这场比赛有些罕见,但显然存在。
-
您使用
'-g'
进行编译,它有一种使bug消失的方法。我重新编译了你的代码,仍然没有改变,但没有'-g'
,竞争变得更加明显:我得到了1000一次,999三次,998两次,997两次,996一次,992一次。 -
关于。添加睡眠的建议是有帮助的,但(a)固定的睡眠时间会使线程仍然按开始时间倾斜(取决于定时器的分辨率),(b)当我们想要将它们拉得更近时,随机睡眠会将它们分散开来。相反,我会对它们进行编码,等待启动信号,这样我就可以在让它们开始工作之前创建它们。有了这个版本(有或没有
'-g'
),我得到的结果到处都是,低至974,不高于998:#include <iostream> #include <thread> #include <vector> using namespace std; unsigned u = 0; bool start = false; void foo() { while (!start) { std::this_thread::yield(); } u++; } int main() { vector<thread> threads; for(int i = 0; i < 1000; i++) { threads.push_back (thread (foo)); } start = true; for (auto& t : threads) t.join(); cout << u << endl; return 0; }
- 为什么"do while"循环不断退出,即使条件计算结果为 false?
- 删除一个线程上有数百万个字符串的大型哈希映射会影响另一个线程的性能
- 在没有太多条件句的情况下,我如何避免被零除
- 基于多个条件处理地图中的所有元素
- 条件constexpr函数
- 无论条件是否为true,if总是在c++中执行
- 我可以使用条件运算符初始化C风格的字符串文字吗
- 基于模板值的条件变量
- 多个If语句与使用逻辑运算符计算条件的单个语句的比较
- 将按位if条件转换为普通if条件
- 条件断点在不应该触发时触发
- 为什么简单的算术减法在"if"条件下不起作用?
- 如何在for循环中包含两个索引值的测试条件
- 为什么擦除方法会影响结束方法
- 如果条件为TRUE(最佳方式?),则在do while循环中后置增量
- 我提出什么条件才能再加5%的折扣
- 循环中的条件:为什么每次都调用strlen(),而vector.size()只调用一次
- 为什么跨线程更改共享变量的代码显然没有受到竞争条件的影响
- 条件操作和大括号会影响代码吗?
- 竞争条件的影响