Can inline substitution cause an infinite loop in multithreaded code?

Viewed 109

Please note: This is just a question out of curiosity, but not about writing better-multithreaded code. I don't and won't write code like this in real projects, of course.

Inline substitution may occur when the inline keyword is added. So I'm curious.

Let's say we have code like this:

static bool done = false;

inline void setDone(bool newState) {
    done = newState;
}

inline bool getDone() {
    return done;
}

void someWorkerThreadJob() {
    // Accessing without any lock/atomic/volatile
    while (getDone() == false) {
    }
}

Can someWorkerThreadJob() be compiled like below and run into an infinite loop?

void someThreadJob() {
    while (done == false) {
    }
}

This also leads me to the next question. What about the getters and setters in classes? Member functions defined inside a class are implicitly inline, so I think inline substitution can occur, thus the same problem. Is this correct?

2 Answers

The access to done must be protected in parallel and synchronized between threads. Otherwise, the processor or the compiler can produce/execute an incorrect sequence of instructions. Your current program is ill-formed.

The problem you are facing is that done can be cached in the L1 CPU cache (processor dependent) or the compiler could optimize the access to the done global variable (typically by putting it in a register, although many compilers do not in practice).

You need to use atomic instructions or mutexes/locks (or any synchronization mechanism) so that done can be seen by other threads when it is modified. With atomic instructions, the compiler generates proper memory fences, do not put done in a register or/and produces instructions that synchronize the communication bus (on x86_64 for example).

For more information, you can give a look to cache-coherence protocols like the MOESI and look how x86 deals with atomics instruction.

In this case, mainstream compilers (like GCC and Clang) actually optimize the code to no-op instruction (which is totally legal here regarding the C++ standard) mainly due to the static and inline keywords helping compilers. This is not the case with std::atomic.

In the situation you've provided, inlining provides no additional problems above and beyond the normal problems of accessing data without locks of some sort. It isn't as if inlining getDone() to just done somehow erases the properties of done into being non-static or something. Like if you coded done in directly as in the example code, there's a risk of an infinite loop, but that'd be true irrespective of inlining or not because cpp's specification leaves this sort of unprotected multi-threaded access to a variable as (I believe) undefined behavior so you might have an infinite loop, you might have a loop that eventually stops if a different thread updates done, and you might have a loop that ~immediately stops if a different thread updates done as if it were protected. Weird things happen in undefined behavior, so there's no easy way to answer what you've asked (AFAIK, I haven't made a thorough investigation of the C++11 spec on this particular point).

Properly implemented inlining doesn't change what is being executed in an unsafe manner because that's sort of the definition of a correct inline. If inlining a method altered its behavior such that it stopped working in a multi-threaded environment, that would be a bug in the compiler that would need to be fixed.

There is, of course, a lot of room for error in compilers and such a bug may very well exist. In practice, an inlining compiler detects situations where inlining might cause a problem and either avoids inlining or fixes the problem. For the most part, inlining is safe and the reason you don't inline is because of ambiguity over what code to inline (in case there's overloading/inheritance/virtual calls going on), recursion (you can't inline a function into itself infinitely, but you can up to a limit), or performance considerations (typically increased code size, but also causing code blocks to become so large that other optimizations are disabled).

Related