std::mutex 用法示例
std::mutex usage example
我写了这段代码作为测试:
#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?
数据竞争是未定义的行为,这意味着任何程序执行都是有效的,包括恰好执行您想要的程序执行。在这种情况下,编译器可能会将您的循环优化为 counter += a
,并且第一个线程在第二个线程开始之前完成,因此它们实际上不会发生冲突。
这里的问题是您的数据竞争非常小。任何现代编译器 will convert your inc
function to counter += a
,所以竞争 window 非常小 - 我什至会说,很可能一旦你启动第二个线程,第一个线程就已经完成了。
这并没有减少任何未定义的行为,而是解释了您看到的结果。您可能会使编译器对您的循环不那么聪明,例如通过使 a
或 k
或 counter
volatile
;那么您的数据竞争应该变得明显。
竞争条件是未定义的行为
当涉及数据竞争时,您无法断言应该发生什么。你断言 应该 有一些可见的数据撕裂证据(即最终结果是 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 的循环也可能非常快。
我写了这段代码作为测试:
#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?
数据竞争是未定义的行为,这意味着任何程序执行都是有效的,包括恰好执行您想要的程序执行。在这种情况下,编译器可能会将您的循环优化为 counter += a
,并且第一个线程在第二个线程开始之前完成,因此它们实际上不会发生冲突。
这里的问题是您的数据竞争非常小。任何现代编译器 will convert your inc
function to counter += a
,所以竞争 window 非常小 - 我什至会说,很可能一旦你启动第二个线程,第一个线程就已经完成了。
这并没有减少任何未定义的行为,而是解释了您看到的结果。您可能会使编译器对您的循环不那么聪明,例如通过使 a
或 k
或 counter
volatile
;那么您的数据竞争应该变得明显。
竞争条件是未定义的行为
当涉及数据竞争时,您无法断言应该发生什么。你断言 应该 有一些可见的数据撕裂证据(即最终结果是 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 的循环也可能非常快。