当 ||在 std::atomic 中使用运算符代替 &&?

How is race condition caused when || operator is used instead of && within the std::atomic?

本文关键字:运算符 std atomic      更新时间:2023-10-16

有一个任务是使 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)

我们在这里看到,打印67后哪个是错误的......

但是,如果我以这种方式检查while (turn <= 2 && turn != 1) {...}则不会出现比赛并且输出始终正确。

类似的竞赛出现在其他函数zeroeven,当它们的内部 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设置为12

even只有在turn == 2时才能离开内部 while 循环。然后,它将turn设置为0

目的是odd只能在turn == 1时离开内部 while 循环(然后将turn设置为0.),但这没有正确实现。

由于离开自旋循环的条件(应该是)互斥的,因此在给定时间不应超过一个线程在其自己的自旋循环之外,因此不应进行并发修改(并且程序应该是无竞争的)。


问题是turn == 0 || turn == 2不是原子的。可能会发生以下情况:

  1. zero完成一次迭代并设置turn = 2

  2. odd检查turn == 0,这是错误的。

  3. 同时,even也看到了turn == 2,退出自旋循环,完成迭代并设置turn = 0

  4. odd现在检查||运算符的右侧:turn == 2,这是假的。

  5. odd离开了它的自旋循环(即使turn == 0!),这当然是无意的,并导致零和奇数之间的竞赛。

简而言之,问题在于||的左侧可能是假的,但在评估右侧时变为真。如果以原子方式评估,则整个turn == 0 || turn == 2在任何时候都不等于false,但由于它不是原子的,因此您可以得到false || truetrue || 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();
相关文章: