Home > Software design >  Multithreading and sequence of instructions
Multithreading and sequence of instructions

Time:12-24

While learning multithread programming I've written the following code.

#include <thread>
#include <iostream>
#include <cassert>

void check() {
    int a = 0;
    int b = 0;
    {
        std::jthread t2([&](){
            int i = 0;
            while (a >= b) {
                  i;
            }
            std::cout << "failed at iteration " << i << "\n"
                      // I know at this point a and b may have changed
                      << a << " >= " << b << "\n";
            std::exit(0);
        });
        std::jthread t1([&](){
            while (true) {
                  a;
                  b;
            }
        });
    }
}

int main() {
    check();
}

Since a always happens before b a should be always greater or equal to b. But experiment shows that sometimes b > a. Why? What causes it? And how can I enforce it?

Even when I replace int a = 0; with int a = 1000; which makes all of this even more crazy.

The program exits soon so no int overflow happens. I found no instructions reordering in assembly which might have caused this.

CodePudding user response:

Since a always happens before b a should be always greater or equal to b

Only in its execution thread. And only if that's observable by the execution thread.

C requires certain explicit "synchronization" in order for changes made by one execution thread be visible by other execution threads.

       a;
       b;

With these statements alone, there are no means by which this execution thread would actually be able to "tell the difference" whether a or b was incremented first. As such, C allows the compiler to implement whatever optimizations or code reordering steps it wants, as long as it has no observable effects in its execution thread, and if the actual generated code incremented b first there will not be any observable effects. There's no way that this execution thread could possibly tell the difference.

But if there was some intervening statement that "looked" at a, then this wouldn't hold true any more, and the compiler would be required to actually generate code that increments a before using it in some way.

And that's just this execution thread, alone. Even if it was possible to observe the relative order of changes to a in b in this execution thread the C compiler is allowed, by the standard, to actually increment the actual variables in any order, as long as there are also any other adjustments that make this not observable. But it could be observable by another execution thread. To prevent that it will be necessary to take explicit synchronization steps, using mutexes, condition variables, and other parts of C 's execution thread model.

CodePudding user response:

There are non-trivial race conditions between the increment of these different variables and when you read them. If you want strict ordering of these reads and writes you will have to use some sort of synchronization mechanism. std::atomic<> makes it easier.

Try this instead:

#include<iostream>
#include <thread>
#include <iostream>
#include <cassert>
#include <atomic>

void check() {
    struct AB { int a = 0; int b=0; };
    std::atomic<AB> ab;
    {
        std::jthread t2([&](){
            int i = 0;
            AB temp;
            while (true) {
                temp = ab;
                if ( temp.a > temp.b ) break;
                  i;
            }
            std::cout << "failed at iteration " << i << "\n"
                      // I know at this point a and b may have changed
                      << temp.a << " >= " << temp.b << "\n";
            std::exit(0);
        });
        std::jthread t1([&](){
            while (true) {
                AB temp = ab;
                temp.a  ;
                temp.b  ;
                ab = temp;
            }
        });
    }
}

int main() {
    check();
}

Code: https://godbolt.org/z/Kxeb8d8or

Result:

Program returned: 143
Program stderr
Killed - processing time exceeded
  • Related