1

Suppose I have two variables:

volatile int a = 0;
int b = 0;

they are shared between two threads. Now in first thread I modify these variables in next order:

a = 1;
b = 2;

in second thread I do:

while (true) {
    if (b == 2)
        assert(a == 1);
}

Is there a guarantee that second thread never fails? Meaning that second thread reads-out written values of a and b in same order that they were written by first thread?

As you can see I made a volatile and b non-volatile. So my question is if volatile modifier makes any guarantee on order of memory writes? And if I make b also volatile will it improve situation?

Or the only way to guarantee order is to use std::atomic<int> for both a and b?

What about std::mutex? If I protect both variables by single shared mutex on both threads and use non-volatile variables will it help on memory ordering? I.e. if I do next (both a and b are non-volatile):

int a = 0, b = 0; // shared
std::mutex m; // shared
// .... In Thread 1 ....
{
    std::unique_lock<std::mutex> l(m);
    a = 1; b = 2;
}
// .... In Thread 2 ....
while (true) {
    std::unique_lock<std::mutex> l(m);
    assert(a == 0 && b == 0 || a == 1 && b == 2);
}

Does above solution of using mutex for non-volatile a and b variables guarantee that assertion never fails, meaning that either a and b are both 0 or set to correct values 1 and 2 same time? Can it happen sometimes that after releasing mutex a and b can be not 1 and 2 for other threads and CPU cores? For example a writing of a is delayed then other core sees a equal to 0 and b equal to 2, can such happen?

I.e. does mutex guarantee memory order and caches propagation between cores? Maybe acquiring/releasing mutex flushes caches or uses some other memory-ordering-enforsing techniques?

Or I have to use std::atomic for all shared variables?

Arty
  • 14,883
  • 6
  • 36
  • 69
  • If you have two 32-bit integers on a 64-bit platform, you can just store them together in a single 64-bit variable and it will be atomic on common platforms like x86-64. Lots of options here, we can't list them all. – John Zwinck Feb 14 '21 at 07:16
  • 6
    The `volatile` keyword is not a synchronization keyword, please don't use it as such. – Some programmer dude Feb 14 '21 at 07:19
  • @JohnZwinck I just put `int` here as an example, in real world I'll have more complex types, not only built-in types, but also complex classes. My question here on what ways I can use to ensure memory ordering? If mutex flushes all caches and ensures ordering of all memory writes? Or I have to use std::atomic always for that? – Arty Feb 14 '21 at 07:22
  • Where did you find information that `volatile` is for multi-threading synchronization? We need find the source of this misinformation and kill it off! – JHBonarius Feb 14 '21 at 08:50
  • @JHBonarius I didn't find it anywhere, I just expected that `volatile` MAY somehow be used for synchronization. Basically it was the purpose of my question to figure out if it really can be used for this or not. At least for some kind of stuff it can be used for sure, and I wanted to find out what is this stuff that `volatile` can be used for. – Arty Feb 14 '21 at 09:30
  • But it seems many people are making that mistake... you must base your expectations on something, right? – JHBonarius Feb 14 '21 at 09:31
  • @JHBonarius My thoughts were such - `volatile` in C++ is used to prevent optimizations of variable because it can be modified from outer sources, I supposed that this means that C++ does some expensive read/write operations when dealing with this variable, next thought was that C++ may use global-only memory read and write, hence this variable updates are propagated to other cores immediately, and possibly (that was my question about) memory ordering is also preserved in such expensive operations. But that were just thoughts, hence I asked my question to clarify all these. – Arty Feb 14 '21 at 09:37
  • @JHBonarius -- it's a Java thing, a Microsoft thing, and an embedded systems thing. – Pete Becker Feb 14 '21 at 14:58

1 Answers1

9

Is there a guarantee that second thread never fails? Meaning that second thread reads-out written values of a and b in same order that they were written by first thread?

No, there is no guarantee of anything at all, in fact. Unsynchronized writing of (non-atomic) variables from one thread and reading them from another invokes undefined behavior, meaning that as far as the compiler is concerned, anything can happen, because the program is broken.

So my question is if volatile modifier makes any guarantee on order of memory writes?

There are two kinds of re-ordering you have to watch out for when dealing with multiple threads:

  1. Re-ordering of instructions at compile-time, by your compiler's optimizer. (e.g. it might change your code to b = 2; a = 1; as part of making your program more efficient, as it is allowed to do under the "as-if" rule)
  2. On-the-fly re-ordering of the generated opcodes at run-time, by the CPU's instruction decoder (also for performance reasons).

The volatile keyword can help you with type (1), but it can't (or at least doesn't) do anything about type (2), and therefore it ends up being insufficient for use in making multithreaded programs work correctly. volatile also doesn't help you at all with cache-coherency issues. For multithreading, you need stronger magic than volatile can provide (which makes sense, since volatile was never intended to be a multithreading construct -- it was intended for simpler use-cases, such as reading memory-mapped device registers)

Or the only way to guarantee order is to use std::atomic for both a and b? What about std::mutex?

Either one of those two approaches should be sufficient to obtain the write-ordering guarantee you are looking for. Only a mutex can provide a more general consistency guarantee, though (see below).

does mutex guarantee memory order and caches propagation between cores?

Yes -- as long as every thread locks the mutex before reading from or writing to shared variables (and unlocks the mutex afterwards), then every thread will see the shared variables in a coherent/consistent state. Memory-order and cache-update-propagation issues will all be handled for you by the mutex implementation (assuming the mutex implementation isn't buggy, which is a reliable assumption these days)

Or I have to use std::atomic for all shared variables?

std::atomic can work, although it only guarantees memory-write ordering; it can't help you if you also need non-trivial consistency guarantees. For example, if thread A needs to set two or more variables, and you need to guarantee that thread B either "sees" all of them set, or sees none of them set (and never sees an interim state where only some of them are set), then you'll need to use a mutex instead.

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • Thanks for detailed answer! Accepted and UpVoted! Can you please also tell me next thing - if I use std::mutex, on both threads, for protecting a lot of variables and classes, does it guarantee that second thread after mutex is released sees all 100% of protected objects in final consistent state? Can it happen at all that mutex-protected variables will be seen only partially, by second thread, part of them is set and part is not? – Arty Feb 14 '21 at 07:33
  • Yes, that is guaranteed -- see the final sentence in my answer. – Jeremy Friesner Feb 14 '21 at 14:52