There is a task to make 3 threads execute always in a specific order like so:
zero // prints 0s only
odd // prints odd numbers
even // prints even numbers
each of the functions (zero, even, odd) are passed to 3 threads respectively, so the output should be:
0102 for n = 2
010203 for n = 3
01020304 for n = 4
and so on
In code:
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;
}
}
};
Lets look at function odd
:
In the inner while loop I need to check if turn
is 0 or 2:
If I the check this way: while (turn == 0 || turn == 2) {...}
the race condition appears with a wrong and not complete output.
for n = 24
it might be:
010203040506709080110100130120150140170160190180210200230220...(waiting)
We see here that after 6
7
is printed which is wrong...
But if I check this way while (turn <= 2 && turn != 1) {...}
no races appears and output is correct always.
Analogous races appear for other functions zero
and even
when their inner while loops are changed to use ||
operator.
I know that combining atomic operations in expression may not necessarily make the whole expression atomic, but I just can't get my head around what scenario can cause race condition with this check while (turn == 0 || turn == 2) {...}
???
Update
Full code example to reproduce the issue:
#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;
}
command to compile:
g++ -std=c++14 -fsanitize=thread -pthread test.cpp -o test
Aucun commentaire:
Enregistrer un commentaire