我已经编写了这段代码作为测试:
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
auto inc(int a) {
for (int k = 0; k < a; ++k)
++counter;
}
int main() {
auto a = std::thread{ inc, 100000 };
auto b = std::thread{ inc, 100000 };
a.join();
b.join();
std::cout << counter;
return 0;
}
counter
变量是全局变量,因此,创建2个线程a
和b
,我希望找到一个数据竞争。输出为200000,而不是随机数。为什么?
此代码是使用mutex
的固定版本,因此全局变量只能访问一次(每次1个线程)。结果仍然是200000。
std::mutex mutex;
auto inc(int a) {
mutex.lock();
for (int k = 0; k < a; ++k)
++counter;
mutex.unlock();
}
事实是这样。互斥锁解决方案给了我200000,这是正确的,因为当时只有1个威胁可以访问计数器。但是为什么非互斥解决方案仍然显示200000?
答案 0 :(得分:6)
这里的问题是您的数据竞争非常小。任何现代的编译器will convert your inc
function to counter += a
,所以竞争窗口非常小-我什至会说,很可能一旦启动第二个线程,第一个线程就已经完成。
这不会使未定义的行为变少,但是可以解释您看到的结果。您可能会降低编译器对循环的了解,例如通过制作a
或k
或counter
volatile
;那么您的数据竞争就会变得显而易见。
答案 1 :(得分:3)
数据争用是未定义的行为,这意味着任何程序执行都是有效的,包括恰好执行您想要执行的程序执行。在这种情况下,编译器可能正在优化进入counter += a
的循环,并且第一个线程在第二个线程开始之前完成,因此它们实际上不会发生冲突。
答案 2 :(得分:2)
您不能断言在涉及数据争用时应该发生什么 。您认为应该有一些明显的数据撕裂证据(即最终结果是178592或类似的东西)是错误的,因为没有理由期待任何此类结果。
以下代码
auto inc(int a) {
for (int k = 0; k < a; ++k)
++counter;
}
可以根据C ++标准合法地优化
auto inc(int a) {
counter += a;
}
请注意,如何将counter
的写入次数从O(a)
优化为O(1)
。这非常重要。这意味着counter
的写操作可能(并且很可能)在第二个线程甚至尚未初始化之前就完成了,从而使统计上的数据撕裂的观察变得不可能。
如果要强制此代码以预期的方式运行,请考虑将变量counter
标记为volatile
:
#include <iostream>
#include <thread>
#include <mutex>
volatile int counter = 0;
auto inc(int a) {
for (int k = 0; k < a; ++k)
++counter;
}
int main() {
auto a = std::thread{ inc, 100000 };
auto b = std::thread{ inc, 100000 };
a.join();
b.join();
std::cout << counter;
return 0;
}
请记住,这仍然是未定义的行为,并且不应在任何以生产为目的的代码中使用!但是,此代码更有可能复制您尝试调用的竞争条件。
您还可以尝试使用大于100000的数字,因为在现代硬件上,即使不进行优化,循环100000也可以很快。