Search code examples
c++multithreadingc++11race-conditionstdatomic

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

Solution

  • For those like me who couldn't glean it immediately from the explanation/code, here is a quick summary:

    zero can only leave the inner while loop if turn == 0. It then sets turn to either 1 or 2.

    even can only leave the inner while loop if turn == 2. It then sets turn to 0.

    The intention is that odd can only leave the inner while loop if turn == 1 (it then sets turn to 0.), but this is not implemented correctly.

    Since the conditions for leaving the spin loops are (supposed to be) mutually exclusive, no more than one thread should be outside its own spin loop at a given time, so no concurrent modifications should be possible (and the program should then be race-free).


    The problem is that turn == 0 || turn == 2 is not atomic. The following can happen:

    1. zero completes one iteration and sets turn = 2.

    2. odd checks turn == 0, which is false.

    3. Meanwhile, even also sees turn == 2, exits the spin loop, completes an iteration and sets turn = 0.

    4. odd now checks the right side of the || operator: turn == 2, which is false.

    5. odd leaves its spin loop (even though turn == 0!) which is of course unintended and leads to a race between zero and odd.

    In short, the problem is that the left hand side of || might be false but become true by the time the right hand side is evaluated. At no point above was the entire turn == 0 || turn == 2 equal to false if evaluated atomically, but since it is not atomic you got a "mix" of false || true and true || false (namely false || false).

    The expression turn <= 2 && turn != 1 does not have this problem as the first condition is always true, and the second condition is the check you really want.


    In the general case, the solution is to read the atomic once, into a local tmp, and check that. That's better for performance because it allows the compiler to optimize your conditions together, or optimize together whatever you're going to do.

        while (true) {
            int t = turn;
            if (!(t == 0 || t == 2)) break;
            yield();
        }
    

    Or perhaps hack that into one line with a comma operator. Comma is a "sequence point" so you can assign to t and then read it. But this is not very human-readable.

        int tmp;
        while (tmp=turn, (tmp == 0 || tmp == 2)) {
            yield();
        }
    

    If you actually wanted to wait for turn to be odd, you could use turn % 2 == 0 or

        while ( turn&1 == 0 )
            yield();