28

I'm implementing Inter-Thread Communication using global variable.

//global var
volatile bool is_true = true;

//thread 1
void thread_1()
{
    while(1){
        int rint = rand() % 10;
        if(is_true) {
            cout << "thread_1: "<< rint <<endl;  //thread_1 prints some stuff
            if(rint == 3)
                is_true = false;  //here, tells thread_2 to start printing stuff
        }
    }
}

//thread 2
void thread_2()
{
    while(1){
        int rint = rand() % 10;
        if(! is_true) {  //if is_true == false
            cout << "thread_1: "<< rint <<endl;  //thread_2 prints some stuff
            if(rint == 7)  //7
                is_true = true;  //here, tells thread_1 to start printing stuff
        }
    }
}

int main()
{
    HANDLE t1 = CreateThread(0,0, thread_1, 0,0,0);
    HANDLE t2 = CreateThread(0,0, thread_2, 0,0,0);
    Sleep(9999999);
    return 0;
}

Question

In the code above, I use a global var volatile bool is_true to switch printing between thread_1 and thread_2.

I wonder whether it is thread-safe to use assignment operation here?

mu is too short
  • 426,620
  • 70
  • 833
  • 800
Alcott
  • 17,905
  • 32
  • 116
  • 173
  • I'd prefer to use an atomic exchange primitive, but I can't work out a scenario in which you'd get a problem... – Kerrek SB Nov 28 '11 at 02:10
  • @KerrekSB, this scenario? Well, I just improvised it to demonstrate my question, :) – Alcott Nov 28 '11 at 02:20
  • Well, I mean a sequence of loads and stores that would be sufficiently broken to make both threads enter the critical section... usually one should be able to demonstrate such a sequence to show why certain code is not correct. I can't see it here, though. I still don't like the code, but I can't prove why. – Kerrek SB Nov 28 '11 at 02:22
  • I think, you can use Different flags, not only one. First flag is to signal from thread1 to thread2 and second flag is to signal from thread2 to thread1. Then you will have two variables with 1 writer each and many readers each. – osgx Nov 28 '11 at 02:58
  • 1
    You cannot use `volatile` for inter-thread communication. (Well, you could in some very particular cases - not here.) – curiousguy Nov 28 '11 at 04:48
  • Similar but without code: http://stackoverflow.com/questions/54188/are-c-reads-and-writes-of-an-int-atomic – Ciro Santilli OurBigBook.com Jun 16 '15 at 10:52

4 Answers4

71

This code is not guaranteed to be thread-safe on Win32, since Win32 guarantees atomicity only for properly-aligned 4-byte and pointer-sized values. bool is not guaranteed to be one of those types. (It is typically a 1-byte type.)

For those who demand an actual example of how this could fail:

Suppose that bool is a 1-byte type. Suppose also that your is_true variable happens to be stored adjacent to another bool variable (let's call it other_bool), so that both of them share the same 4-byte line. For concreteness, let's say that is_true is at address 0x1000 and other_bool is at address 0x1001. Suppose that both values are initially false, and one thread decides to update is_true at the same time another thread tries to update other_bool. The following sequence of operations can occur:

  • Thread 1 prepares to set is_true to true by loading the 4-byte value containing is_true and other_bool. Thread 1 reads 0x00000000.
  • Thread 2 prepares to set other_bool to true by loading the 4-byte value containing is_true and other_bool. Thread 2 reads 0x00000000.
  • Thread 1 updates the byte in the 4-byte value corresponding to is_true, producing 0x00000001.
  • Thread 2 updates the byte in the 4-byte value corresponding to other_bool, producing 0x00000100.
  • Thread 1 stores the updated value to memory. is_true is now true and other_bool is now false.
  • Thread 2 stores the updated value to memory. is_true is now false and other_bool is now true.

Observe that at the end this sequence, the update to is_true was lost, because it was overwritten by thread 2, which captured an old value of is_true.

It so happens that x86 is very forgiving of this type of error because it supports byte-granular updates and has a very tight memory model. Other Win32 processors are not as forgiving. RISC chips, for example, often do not support byte-granular updates, and even if they do, they usually have very weak memory models.

Raymond Chen
  • 44,448
  • 11
  • 96
  • 135
  • Great explanation about the alignment. Your example cleanly shows that assignment is not atomic. – Pavel Gatilov Nov 28 '11 at 03:55
  • 3
    BTW, wouldn't the `volatile` keyword force a compiler to align the variable in a thread-safe way for the targeted architecture (i.e. to store the `bool` value in a 4-byte cell for 32-bit x86 processors)? This would solve the issue. Do some of the compilers do this trick? – Pavel Gatilov Nov 28 '11 at 05:26
  • 1
    @PavelGatilov according to the latest draft standard, the compiler must prevent this behaviour. – curiousguy Nov 28 '11 at 08:55
  • Does this mean that critical sections are useless when trying to protect values that are not " properly-aligned 4-byte and pointer-sized" since code writing to nearby memory might not acquire the lock? – IronMensan Nov 28 '11 at 08:57
  • @IronMensan If your compiler is non-conforming, yes. – curiousguy Nov 28 '11 at 09:04
  • 2
    This explanation sounds nice, but it's a bit worrying: If I have two separate threads that use entirely unrelated global variables, then according to your reasoning I should still be worried that the two threads garble each other's data. That doesn't sound right. – Kerrek SB Nov 28 '11 at 12:15
  • 4
    @KerrekSB Life is strange when you operate on sub-atomic particles. – Raymond Chen Nov 28 '11 at 14:59
  • 2
    @RaymondChen: Yes. Luckily this isn't a question about QFT, but about C++! – Kerrek SB Nov 28 '11 at 15:00
  • "_RISC chips, for example, often do not support byte-granular updates_" can you name those chips? – curiousguy Nov 29 '11 at 02:27
  • The original Alpha AXP and the original ARM to name two. – Raymond Chen Nov 29 '11 at 02:33
  • So assignment of `int` is atomic in win32? – choxsword Apr 03 '18 at 04:56
7

no, its not..... you need to use a locking primitive of some sort. Depending on platform, you can either use boost ones, or if going native windows, something like InterlockedCompareExchange.

In fact in your situation you might what to use some of the thread safe event mechanisims so you can 'signal' your other thread to start doing what you want.

Keith Nicholas
  • 43,549
  • 15
  • 93
  • 156
  • 2
    What's *wrong* with the OP's code, though? Can you devise a scenario where something breaks? – Kerrek SB Nov 28 '11 at 02:15
  • @KerrekSB, excuse me, OP? what is that? – Alcott Nov 28 '11 at 02:22
  • 2
    @Alcott: "OP" means "original poster", as in, the person who asked the question. In this case, it's you. :-) – ruakh Nov 28 '11 at 02:28
  • Locking primitives are meant for situations where there is competition between 2 or more threads. In the code given here there is no such thing. thread_1 works in critical section until it rolls certain random number. Then it changes global var and leaves. Only after that other thread can enter. – Pavel Zhuravlev Nov 28 '11 at 03:17
  • @KerrekSB "_What's wrong with the OP's code, though?_" what's the point of `volatile` here? – curiousguy Nov 28 '11 at 04:50
  • "_you need to use a locking primitive of some sort_" no, you need a synchronisation primitive – curiousguy Nov 28 '11 at 04:50
  • 1
    @curiousguy: missing the point... my question is, "How do you break the OP's code?" Can you write down an execution sequence that leads to broken behaviour? – Kerrek SB Nov 28 '11 at 12:12
  • @KerrekSB **you** are missing my point. What is the use of `volatile` here? – curiousguy Nov 28 '11 at 12:16
4

On all modern processors, you can assume that reads and writes of naturally aligned native types are atomic. As long as the memory bus is at least as wide as the type being read or written, the CPU reads and writes these types in a single bus transaction, making it impossible for other threads to see them in a half-completed state. On x86 and x64 there, is no guarantee that reads and writes larger than eight bytes are atomic. This means that 16-byte reads and writes of streaming SIMD extension (SSE) registers, and string operations, might not be atomic.

Reads and writes of types that are not naturally aligned—for instance, writing DWORDs that cross four-byte boundaries—are not guaranteed to be atomic. The CPU may have to do these reads and writes as multiple bus transactions, which could allow another thread to modify or see the data in the middle of the read or write.

themadme
  • 51
  • 6
-2

The thread safeness of this piece of code does not depend on atomicity of the assignment. Both thread routines work strictly in turn. There is no race condition: thread_1 will output stuff until getting certain random number after which it will leave the 'output section' and let the other thread work in it. There are a couple of things worth noting though:

  • rand() function may be not thread-safe (not the problem in the code given here though)
  • you should not use Win32 function CreateThread(), especially when you are using CRT libraly functions which (potentially) utilize global variables. Use _beginthreadex() instead.