Is it dangerous to read global variables from separate threads at potentially the same time?
Asked Answered
F

8

8

So I'm writing this neat little program to teach myself threading, I'm using boost::thread and C++ to do so.

I need the main thread to communicate with the worker thread, and to do so I have been using global variables. It is working as expected, but I can't help but feel a bit uneasy.

What if the the worker thread tries write to a global variable at the same time as the main thread is reading the value. Is this bad, dangerous, or hopefully taken into account behind the scenes??

Feaster answered 18/2, 2015 at 18:40 Comment(4)
It's not dangerous to read global variables concurrently from multiple threads (assuming basic data types - more complex data structures could involve updating state during what appears to be a read operation). However, if the variable is being written to, that's a very different story.Corymb
This is undefined behaviour, and yes, quite dangerous in practice, depending on your platform. In particular, even if the type is of a machine word size and your CPU gives you atomic reads/writes (assuming proper alignment) to machine words, the memory ordering of other reads/writes will almost certainly not be what you expect.Achromic
Reading in absence of writing is always safe. Reading while another thread writes is, under normal conditions, practically safe with the exception of not guaranteeing a particular order (since all CPUs work with caches and cannot access memory non-atomically except if you cross a cacheline -- which won't happen if you have standard alignment) but is not defined by the C++ standard. Note that writing to and e.g. incrementing a variable are very different things (writes being practically safe but not theoretically, and read-modify-write operations being not safe at all).Disturbance
The op clearly states both writing and reading at the same time.Ashla
N
12

§1.10 [intro.multithread] (quoting N4140):

6 Two expression evaluations conflict if one of them modifies a memory location (1.7) and the other one accesses or modifies the same memory location.

23 Two actions are potentially concurrent if

  • they are performed by different threads, or
  • they are unsequenced, and at least one is performed by a signal handler.

The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

Purely concurrent reads do not conflict, and so is safe.

If at least one of the threads write to a memory location, and another reads from that location, then they conflict and are potentially concurrent. The result is a data race, and hence undefined behavior, unless appropriate synchronization is used, either by using atomic operations for all reads and writes, or by using synchronization primitives to establish a happens before relationship between the read and the write.

Neona answered 18/2, 2015 at 19:12 Comment(5)
Well, the global variable I am talking about is simply storing an integer. It contains the position of some sensor and gets updated by the worker thread. The main thread can look at that value and depending on what the value is it does some more work. I'm researching atomic operations atm, so I'm not too sure if that counts as one.Feaster
@Feaster Unless you are using something in <atomic> (or the boost equivalent), it isn't.Neona
So I can solve this by adding a mutex lock whenever the worker thread is writing to the global var?Feaster
@ch0l1n3: Sounds like all you need is to replace the int with a std::atomic<int>.Tingly
hi @Neona what does unsequenced mean in this quote? thanksCoadjutor
H
4

If your different threads only read values of global variables, there will be no problem.

If more than one thread tries to update same variable (example read, add 1 write), then you must use a synchronization system to ensure that the value cannot be modified between the read and the write.

If only one thread writes while others read, it depends. If the different variables are unrelated, say number of apples and oranges in a basket, you do not need any synchronization, provided you accept not exactly accurate values. But if the values are related say amount of money on two bank accounts with a transfert between them, you need synchronization to ensure that what you read is coherent. It could be too old when you use it because it has already be updated but you have coherent values.

Holtorf answered 18/2, 2015 at 18:52 Comment(2)
"If only one thread writes while others read" - in the absence of synchronization, unless nobody reads what that thread wrote, it's UB, period.Neona
Ok! So since I don't care about synchronization, then it's perfect! I was just afraid the value would get cut off because the worker thread hasn't finished writing, etc.Feaster
A
2

The simple answer is yes. Once variables are starting to be shared amongs multiple threads for both reading and writing you will need some kind of protection. There are different flavours to achieve this : Semaphores, locks, mutex, events, critical section message queues. Especially when your globals are references things can become ugly. Suppose you have global list of objects in a consumers / producers scenario with multiple consumers, the producer instantiates objects, the consumer takes them, does something with them and finally disposes them, without protection of some sort this leads to terrible problems. There is a lot of specialised literature about this topic and there are dedicated college courses about this topic, and well known problems that are being given to students. For instance the dining philosefers problem, howto make a readerswritersemaphore without starvation, ... . Interesting book : the little book about semaphores.

Ashla answered 18/2, 2015 at 18:43 Comment(3)
oh, then a simple yes would have been sufficient.Ashla
Now that it says "Yes": downvote is changed to upvote and comment removed.Tingly
I try to improve if I can.Ashla
D
2

Yes. No. Maybe.

The formally correct answer is: This is not safe.

The practical answer is not that easy. It's something like "This is safe, kind of, under some conditions".

Reads (any number of them) in absence of concurrent writes are always safe. Reads (even a single one) in presence of concurrent writes (even a single one) are formally never safe, but they are atomic on most processors in most situations, and this can be just good enough. Changing values (like incrementing a counter) is nearly always troublesome, even in practice, without explicitly using atomic operations.

Atomicity

The C++ standard mandates that you use std::atomic or one of its specializations (or higher level synchronization primitives), or you are doomed. Demons will fly out of your nose (no, they won't... but as far as the standard goes, they might as well).

All real, non-theoretical CPUs access memory exclusively via cache lines except in very special conditions which you must expclicitly provoke (such as using write-combining instructions). An entire cache line can be read or written to, atomically, at a time -- never anything different. Reading any memory location that is being written to might not give the value that you expect (if it has been updated in the mean time), but it will never return a "garbage" value.
Now of course a variable might cross a cacheline, in which case access isn't atomic, but unless you deliberately provoke it, this will not happen (since integral variables are power-of-two sized such as 2, or 4, or 8, and cache lines are also power-of-two sized and larger such as 64 or 128 -- if your variables are properly aligned to the former as by default, they are automatically also completely contained within the latter. Always.).

Ordering

Although your reads (and writes) may be atomic, and you might say that you only care whether some flag is zero or not so who cares even if a value is garbled, you don't have a guarantee that things happen in the order that you expect!
The "normal" expectation that if you say that A happens before B, then A indeed happens before B and A can be seen by someone else before B is generally not true. In other words, it is perfectly possible that your worker thread prepares some data and then sets the ready flag. Your main thread sees that the ready flag is set, and begins reading some random garbage while the real data is still on its way somewhere in the cache hierarchy. Or maybe half of it is visible to the main thread already, but the other half isn't.

For this, C++11 introduced the concept of memory order. This means no more and no less than besides having the guarantee of atomicity, you also have a way of requesting a happens-before guarantee.
Most of the time, this only prevents the compiler from moving around loads and stores, but on some architectures, it may cause special instructions to be emitted (that's not your problem, though).

Read-Modify-Write

This is a particularly nefarious one. A simple thing like ++flag; can be desastrous. This is not at all the same as flag = 1;

Without using proper atomic instructions, this is never safe, as it involves (atomically) reading, then modifying, and then (atomically) writing a cache line.
The problem is, while reading and writing are both atomic, the whole thing isn't. Nor is there any guarantee about ordering.

Solution?

Either use std::atomic or block on a condition variable. The former will involve spinning, which may or may not be detrimental (depending on the frequency and latency requirements) while the latter will be CPU conservative.
You could use a mutex to synchronize access to the global variable, too, but if you involve a heavyweight primitive, you might as well go for the condition variable instead of spinning (which will be the "correct" approach).

Disturbance answered 18/2, 2015 at 19:44 Comment(0)
I
1

This really depends on a number of factors but is generally a bad idea and can lead to race conditions. You can avoid this by locking the value so that reads and writes are all atomic and thus can't collide.

Immanent answered 18/2, 2015 at 18:44 Comment(0)
F
0

You must create a mutex (mutual exclusion object), only one thread at a time can own the mutex, and use it to control access to the variables. https://msdn.microsoft.com/en-us/library/z3x8b09y.aspx

Florey answered 18/2, 2015 at 18:45 Comment(4)
But mutexes are used for simultaneous writes right?Feaster
@ch0l1n3: No, you need mutexes for a write+anything.Tingly
They can be used for anything that needs to prevent multiple threads from accessing the same resource at the same time, all it does is refusing to be re-acquired before it's been released, there's nothing more to it.Florey
Okay, why are correct answers being downvoted on this thread?Florey
S
0

This actually points to a race condition between writer thread and reader thread. The places where we access/write the global variable would be the critical sections of the code. Ideally we must synchronize between the read/write threads whenever we operate in the critical sections or else we may see unspecific behavior in the code.

Your problem is similar to a reader-writer problem and we must synchronize using semaphores, mutex and other locking mechanisms to avoid the race condition. Assuming 1 writer and multiple reader we may use the following code to avoid undefined behavior:

// Using read and write semaphores
semaphore rd, wrt; 
int readCount;

// Writer Thread 

do
{
...
// Critical Section Starts  

wait(wrt);
    global variable = someValues;   // Write to the global Variable.
signal(wrt);

// Critical Section Ends  
...
} while(1)


// Reader thread 

do
{
...
// Critical Section 1 Starts  

wait(rd)
readcount++;
    if(readCount == 1) 
        wait(wrt);
signal(rd);

// Critical Section 1 Ends

// Do Reading 

// Critical Section 2 Starts
wait(rd)  
    readcount--;
    if(readCount == 0)
        signal(wrt);
signal(rd)
// Critical Section 2 Ends
...
} while(1)
Swenson answered 18/2, 2015 at 19:51 Comment(0)
H
-1

Concurrent writes are not safe. Concurrent read and write are always safe (assuming atomic writes), but you never know whether you've read the value before or after write.

Main thread behaves just the same as spawned threads, there's no difference at all.

So, for concurrent write you'll need mutexes.

Halliburton answered 18/2, 2015 at 18:44 Comment(3)
Ok! I have no plans for simultaneous writes, and it doesn't affect me whether I read before or after. So I guess I don't need mutexes, your answer corroborates with one of my co-worker (who just got to the lab).Feaster
Concurrent read+write is not safe unless the write is atomic. If the write is not atomic, then read+write is not safe. If I have a variable 0x0F, and one thread reads it at the same time a second thread writes 0x10, then it could be the 0x1 gets writen, then the first thread reads 0x1F, then the second thread writes the 0xF. Thus, the read thread got a completely unsafe invalid value.Tingly
My bad! I was unaware. I'll edit the answer, thanks for pointing it out.Halliburton

© 2022 - 2024 — McMap. All rights reserved.