Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the threading guarantees of nowadays C and C++ compilers?

I'm wondering what are the guarantees that compilers make to ensure that threaded writes to memory have visible effects in other threads.

I know countless cases in which this is problematic, and I'm sure that if you're interested in answering you know it too, but please focus on the cases I'll be presenting.

More precisely, I am concerned about the circumstances that can lead to threads missing memory updates done by other threads. I don't care (at this point) if the updates are non-atomic or badly synchronized: as long as the concerned threads notice the changes, I'll be happy.

I hope that compilers makes the distinction between two kinds of variable accesses:

  • Accesses to variables that necessarily have an address;
  • Accesses to variables that don't necessarily have an address.

For instance, if you take this snippet:

void sleepingbeauty()
{
    int i = 1;
    while (i) sleep(1);
}

Since i is a local, I assume that my compiler can optimize it away, and just let the sleeping beauty fall to eternal slumber.

void onedaymyprincewillcome(int* i);

void sleepingbeauty()
{
    int i = 1;
    onedaymyprincewillcome(&i);
    while (i) sleep(1);
}

Since i is a local, but its address is taken and passed to another function, I assume that my compiler will now know that it's an "addressable" variable, and generate memory reads to it to ensure that maybe some day the prince will come.

int i = 1;
void sleepingbeauty()
{
    while (i) sleep(1);
}

Since i is a global, I assume that my compiler knows the variable has an address and will generate reads to it instead of caching the value.

void sleepingbeauty(int* ptr)
{
    *ptr = 1;
    while (*ptr) sleep(1);
}

I hope that the dereference operator is explicit enough to have my compiler generate a memory read on each loop iteration.

I'm fairly sure that this is the memory access model used by every C and C++ compiler in production out there, but I don't think there are any guarantees. In fact, the C++03 is even blind to the existence of threads, so this question wouldn't even make sense with the standard in mind. I'm not sure about C, though.

Is there some documentation out there that specifies if I'm right or wrong? I know these are muddy waters since these may not be on standards grounds, it seems like an important issue to me.

Besides the compiler generating reads, I'm also worried that the CPU cache could technically retain an outdated value, and that even though my compiler did its best to bring the reads and writes about, the values never synchronise between threads. Can this happen?

like image 877
zneak Avatar asked Dec 21 '22 12:12

zneak


2 Answers

Accesses to variables that don't necessarily have an address.

All variables must have addresses (from the language's prospective -- compilers are allowed to avoid giving things addresses if they can, but that's not visible from inside the language). It's a side effect that everything must be "pointerable" that everything has an address -- even the empty class typically has size of at least a char so that a pointer can be created to it.

Since i is a local, but its address is taken and passed to another function, I assume that my compiler will now know that it's an "addressable" variables, and generate memory reads to it to ensure that maybe some day the prince will come.

That depends on the content of onedaymyprincewillcome. The compiler may inline that function if it wishes and still make no memory reads.

Since i is a global, I assume that my compiler knows the variable has an address and will generate reads to it.

Yes, but it really doesn't matter if there are reads to it. These reads might simply be going to cache on your current local CPU core, not actually going all the way back to main memory. You would need something like a memory barrier for this, and no C++ compiler is going to do that for you.

I hope that the dereference operator is explicit enough to have my compiler generate a memory read on each loop iteration.

Nope -- not required. The function may be inlined, which would allow the compiler to completely remove these things if it so desires.

The only language feature in the standard that lets you control things like this w.r.t. threading is volatile, which simply requires that the compiler generate reads. That does not mean the value will be consistent though because of the CPU cache issue -- you need memory barriers for that.

If you need true multithreading correctness, you're going to be using some platform specific library to generate memory barriers and things like that, or you're going to need a C++0x compiler which supports std::atomic, which does make these kinds of requirements on variables explicit.

like image 113
Billy ONeal Avatar answered Feb 09 '23 08:02

Billy ONeal


You assume wrong.

void onedaymyprincewillcome(int* i);

void sleepingbeauty()
{
    int i = 1;
    onedaymyprincewillcome(&i);
    while (i) sleep(1);
}

In this code, your compiler will load i from memory each time through the loop. Why? NOT because it thinks another thread could alter its value, but because it thinks that sleep could modify its value. It has nothing to do with whether or not i has an address or must have an address, and everything to do with the operations that this thread performs which could modify the code.

In particular, it is not guaranteed that assigning to an int is even atomic, although this happens to be true on all platforms we use these days.

Too many things go wrong if you don't use the proper synchronization primitives for your threaded programs. For example,

char *str = 0;
asynch_get_string(&str);
while (!str)
    sleep(1);
puts(str);

This could (and even will, on some platforms) sometimes print out utter garbage and crash the program. It looks safe, but because you are not using the proper synchronization primitives, the change to ptr could be seen by your thread before the change to the memory location it refers to, even though the other thread initializes the string before setting the pointer.

So just don't, don't, don't do this kind of stuff. And no, volatile is not a fix.

Summary: The basic problem is that the compiler only changes what order the instructions go in, and where the load and store operations go. This is not enough to guarantee thread safety in general, because the processor is free to change the order of loads and stores, and the order of loads and stores is not preserved between processors. In order to ensure things happen in the right order, you need memory barriers. You can either write the assembly yourself or you can use a mutex / semaphore / critical section / etc, which does the right thing for you.

like image 39
Dietrich Epp Avatar answered Feb 09 '23 06:02

Dietrich Epp