jeudi 24 octobre 2019

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

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