当 ||在 std::atomic 中使用运算符代替 &&?
How is race condition caused when || operator is used instead of && within the std::atomic?
有一个任务是使 3 个线程始终以特定顺序执行,如下所示:
zero // prints 0s only
odd // prints odd numbers
even // prints even numbers
每个函数(零、偶数、奇数)分别传递给 3 个线程,因此输出应为:
0102 for n = 2
010203 for n = 3
01020304 for n = 4
and so on
在代码中:
class ZeroEvenOdd {
private:
int n;
std::atomic<int> turn{0};
bool flag = false;
public:
ZeroEvenOdd(int n) {
this->n = n;
}
void zero(std::function<void(int)> printNumber) {
int i = 0;
while (i < n) {
while (turn > 0) {// 1 or 2
std::this_thread::yield();
}
printNumber(0);
turn = !flag ? 1 : 2;
flag = !flag;
++i;
}
}
void even(std::function<void(int)> printNumber) {
int i = 2;
while (i <= n) {
while (turn < 2) {// 0 or 1
std::this_thread::yield();
}
printNumber(i);
turn = 0;
i += 2;
}
}
void odd(std::function<void(int)> printNumber) {
int i = 1;
while (i <= n) {
//while (turn <= 2 && turn != 1) {// 0 or 2 // how does this expression eliminate the race ???
while (turn == 0 || turn == 2) { // this causes race condition
std::this_thread::yield();
}
printNumber(i);
turn = 0;
i += 2;
}
}
};
让我们看一下函数odd
:
在内部 while 循环中,我需要检查turn
是 0 还是 2:
如果我以这种方式检查:while (turn == 0 || turn == 2) {...}
竞争条件出现错误且不完整的输出。
for n = 24
it might be:
010203040506709080110100130120150140170160190180210200230220...(waiting)
我们在这里看到,打印6
7
后哪个是错误的......
但是,如果我以这种方式检查while (turn <= 2 && turn != 1) {...}
则不会出现比赛并且输出始终正确。
类似的竞赛出现在其他函数zero
和even
,当它们的内部 while 循环更改为使用运算符时||
。
我知道在表达式中组合原子操作不一定会使整个表达式原子化,但我只是无法弄清楚什么情况会导致此检查while (turn == 0 || turn == 2) {...}
???的竞争条件
更新
重现问题的完整代码示例:
#include <iostream>
#include <thread>
#include <atomic>
#include <functional>
class ZeroEvenOdd {
private:
int n;
std::atomic<int> turn{0};
bool flag = false;
public:
ZeroEvenOdd(int n) {
this->n = n;
}
void zero(std::function<void(int)> printNumber) {
int i = 0;
while (i < n) {
while (turn > 0) {// 1 or 2
std::this_thread::yield();
}
printNumber(0);
turn = !flag ? 1 : 2;
flag = !flag;
++i;
}
}
void even(std::function<void(int)> printNumber) {
int i = 2;
while (i <= n) {
while (turn < 2) {// 0 or 1
std::this_thread::yield();
}
printNumber(i);
turn = 0;
i += 2;
}
}
void odd(std::function<void(int)> printNumber) {
int i = 1;
while (i <= n) {
//while (turn <= 2 && turn != 1) {// 0 or 2 // how does this expression eliminate the race ???
while (turn == 0 || turn == 2) { // this causes race condition
std::this_thread::yield();
}
printNumber(i);
turn = 0;
i += 2;
}
}
};
int main() {
int n = 24;
std::function<void(int)> printNum = [](int x) {
std::cout << x << std::flush;
};
ZeroEvenOdd zeroEvenOdd(n);
std::thread t1(&ZeroEvenOdd::zero, &zeroEvenOdd, printNum);
std::thread t2(&ZeroEvenOdd::even, &zeroEvenOdd, printNum);
std::thread t3(&ZeroEvenOdd::odd, &zeroEvenOdd, printNum);
t1.join();
t2.join();
t3.join();
return 0;
}
要编译的命令:
g++ -std=c++14 -fsanitize=thread -pthread test.cpp -o test
对于那些像我这样无法立即从解释/代码中收集它的人,这里有一个快速摘要:
zero
只有在turn == 0
时才能离开内部 while 循环。然后,它将turn
设置为1
或2
。
even
只有在turn == 2
时才能离开内部 while 循环。然后,它将turn
设置为0
。
目的是odd
只能在turn == 1
时离开内部 while 循环(然后将turn
设置为0
.),但这没有正确实现。
由于离开自旋循环的条件(应该是)互斥的,因此在给定时间不应超过一个线程在其自己的自旋循环之外,因此不应进行并发修改(并且程序应该是无竞争的)。
问题是turn == 0 || turn == 2
不是原子的。可能会发生以下情况:
-
zero
完成一次迭代并设置turn = 2
。 -
odd
检查turn == 0
,这是错误的。 -
同时,
even
也看到了turn == 2
,退出自旋循环,完成迭代并设置turn = 0
。 -
odd
现在检查||
运算符的右侧:turn == 2
,这是假的。 -
odd
离开了它的自旋循环(即使turn == 0
!),这当然是无意的,并导致零和奇数之间的竞赛。
简而言之,问题在于||
的左侧可能是假的,但在评估右侧时变为真。如果以原子方式评估,则整个turn == 0 || turn == 2
在任何时候都不等于false
,但由于它不是原子的,因此您可以得到false || true
和true || false
的"混合"(即false || false
)。
表达式turn <= 2 && turn != 1
没有这个问题,因为第一个条件总是true
,第二个条件是你真正想要的检查。
在一般情况下,解决方案是将原子读取一次,进入本地tmp,然后检查。 这对性能更好,因为它允许编译器一起优化你的条件,或者一起优化你要做什么。
while (true) {
int t = turn;
if (!(t == 0 || t == 2)) break;
yield();
}
或者也许用逗号运算符将其破解为一行。 逗号是一个"序列点",因此您可以分配给t
然后读取它。 但这不是很人性化。
int tmp;
while (tmp=turn, (tmp == 0 || tmp == 2)) {
yield();
}
如果你真的想等待turn
是奇数,你可以使用turn % 2 == 0
或
while ( turn&1 == 0 )
yield();
- <T> 通过模板化运算符重载将 std::complex 乘以双倍
- 使用运算符 [] 引用 std::vector 上最后一个元素时出现问题<>
- 关于 std::min, std::max 中的比较运算符的混淆
- C++:无法使用 "=" 运算符更改 std::p air 的值
- 为 std::variant 提供一个运算符 ==
- 错误 C2679:二进制"<<":未找到采用类型 'std::string_view' 的右侧操作数的运算符(或者没有可接受的转换)
- C++ STD 函数运算符:有没有一种方法可以通过函数将一个向量映射到另一个向量上?
- 体系结构x86_64的未定义符号:std:terminate(),typeinfo,运算符delete[],运算符new
- C++矩阵类运算符使用 std::common_type_t 和复数的实现
- 错误:为"运算符 std::string {aka std::__cxx11::basic_string}"指定的返回类型<char>
- 运算符/ STD :: Chrono ::持续时间和自定义类型与Clang
- ostream 和运算符 std::basic_string<char, std::char_traits<char>, std::分配器<char>>?
- 为什么显式运算符 std::string 不起作用
- 运算符< std::set 的重载
- 运算符< std::map 的 int 类型比较的重载?(我希望它按降序排序。
- 运算符=(std::p romise&&) 在 C++11 中的结果是什么?
- 绑定运算符=std::string的成员
- 传递 const std::auto_ptr<> 作为 std::auto_ptr<_Tp>::运算符 std::auto_ptr_ref<_Tp1>() 的参数丢
- 运算符<< std::stringstream 派生类的重载(只是)
- 重载运算符 std::ostream& 运算符<<打印实例内存地址