Partial Thread Synchronisation

Started by
18 comments, last by the_edd 12 years, 11 months ago
Are you using CRITICAL_SECTIONs yet? How are you sending/receiving data between the threads. Typically, you'd set up a producer/consumer queue using condition variables or Windows Events. Do you have that?
Advertisement

EDIT: By the way, you can't (reliably) have threads communicate with each other like that:


while ( !g_terminated )
{
}


Any and all concurrent access to shared data must use some form of sychronization, or else the results are undefined. Each time you want to inspect or alter the value of your g_terminated variable, you must first acquire some kind of lock (read up on critical sections ... use the msdn online documentation). Otherwise all kinds of weird things can happen, such as one thread changing the value of g_terminated but the other thread never seeing the change.


In that specific case, what could go wrong?

If the child thread is simply checking whether g_terminated has been set to true, I can't see how it could cause synchronization problems. If someone was reading the value, doing something with it, and then setting another value - then we'd have a synchronization problem.

From Interlocked Variable Access (Windows) on MSDN:

Simple reads and writes to properly-aligned 32-bit variables are atomic operations. In other words, you will not end up with only one portion of the variable updated; all bits are updated in an atomic fashion. However, access is not guaranteed to be synchronized. If two threads are reading and writing from the same variable, you cannot determine if one thread will perform its read operation before the other performs its write operation.
[/quote]

Busy-polling a variable might be a bad idea for other reasons, like hogging up excessive CPU resources.

[quote name='Red Ant' timestamp='1303248265' post='4800520']
EDIT: By the way, you can't (reliably) have threads communicate with each other like that:


while ( !g_terminated )
{
}


Any and all concurrent access to shared data must use some form of sychronization, or else the results are undefined. Each time you want to inspect or alter the value of your g_terminated variable, you must first acquire some kind of lock (read up on critical sections ... use the msdn online documentation). Otherwise all kinds of weird things can happen, such as one thread changing the value of g_terminated but the other thread never seeing the change.


In that specific case, what could go wrong?

If the child thread is simply checking whether g_terminated has been set to true, I can't see how it could cause synchronization problems. If someone was reading the value, doing something with it, and then setting another value - then we'd have a synchronization problem.
[/quote]

Are you aware of the memory visibility issues that doing so could entail? For instance, the OS might decide that thread A should move g_terminated to some kind of cache and then access that for increased speed. Meanwhile, thread B (whose version of g_terminated resides in RAM) modifies g_terminated. However, it is not aware that thread A has moved its version to a cache, so thread A never gets to see the changed value of g_terminated.
Ok, I do believe I have fixed it all. Critical sections were't working for me, but mutexes worked like a charm. Problem resolved!

Ok, I do believe I have fixed it all. Critical sections were't working for me, but mutexes worked like a charm. Problem resolved!


Beware that compared to CRITICAL_SECTIONS, mutexes (or what Windows calls mutexes anyway) are relatively heavy-weight. At any rate, there is no reason why critical sections shouldn't "work for you". Can you elaborate on what kind of problems you've run in with them?

Are you aware of the memory visibility issues that doing so could entail? For instance, the OS might decide that thread A should move g_terminated to some kind of cache and then access that for increased speed. Meanwhile, thread B (whose version of g_terminated resides in RAM) modifies g_terminated. However, it is not aware that thread A has moved its version to a cache, so thread A never gets to see the changed value of g_terminated.


I'm pretty sure cache coherency is ensured on all modern multicore system?

It's not guaranteed which order the caches will be notified if you change a variable like in the above example, but they'll all know it eventually. If thread A is just waiting for thread B to set a bool to true, it will get the new value eventually when the cache is updated.

Some quick googling led to this: http://en.wikipedia.org/wiki/MESI_protocol

It's not guaranteed which order the caches will be notified if you change a variable like in the above example, but they'll all know it eventually. If thread A is just waiting for thread B to set a bool to true, it will get the new value eventually when the cache is updated.
In the specific case of [font="CourierNew, monospace"]g_terminated[/font], caching is probably not an issue (on any multi-core architecture I've used), but in other cases it's a crash waiting to happen, e.g.//thread A
g_pData = new Data();

//thread B
if( g_pData )
g_pData->DoStuff()
In the above case, the pointer value could become visible to therad B's caches before the actual values that are pointed to by the pointer.
i.e. the pointer assignment could appear to happen before the constructor!
It's good practice to always ensure proper thread synchronisation/ordering, unless you've got a very good reason to risk dealing with out of order memory issues.

However, going back to [font="CourierNew, monospace"]while( !g_terminated )[/font], if [font="CourierNew, monospace"]g_terminated[/font] isn't volatile, then the compiler can rightfully decide to optimise that code to fetch the value only once://pseudo machine code
bool fetch = ReadRam(g_terminated);
while( !fetch )
instead of what you want - fetching it each iteration:bool fetch;
while( !(fetch = ReadRam(g_terminated)) )
which would cause an infinite loop (but because of bad assembly, not caches ;))
Again, best to just do the right thing and wrap your shared data up in a critical-section/mutex to avoid these problems (unless you've got a good reason not to).

N.B. A critical-section / mutex will have the same effect on the compiler as volatile will - it will stop the optimizer from moving the read instruction across the critical section's boundary -- if you're doing things at a high level and by the book, you shouldn't ever have to use the volatile keyword yourself.

Again, best to just do the right thing and wrap your shared data up in a critical-section/mutex to avoid these problems (unless you've got a good reason not to).


Yes yes, I agree with you 99%. My only caveat is that critical sections and mutexes can be quite heavy if overused. Atomic variables with InterlockedExchange()/whatever are often more efficient. Also, the best way to deal with synchronization is to avoid having to do it :)

Good point about remembering the volatile keyword when needed, that fixes one potential bug in the code base I'm working on now :P
if you can't afford wait times (mutexes and timers cause them), you can use round-robins so that only one thread is doing work on something at a time

thread a:

if (a == 0)
do work
a = 1;


thread b:

if (a == 1)
do work unless too much time is spent
if (the work is completely done) a == 0;

if you can't afford wait times (mutexes and timers cause them), you can use round-robins so that only one thread is doing work on something at a time

thread a:

if (a == 0)
do work
a = 1;


thread b:

if (a == 1)
do work unless too much time is spent
if (the work is completely done) a == 0;


This will only work sporadically unless memory barriers are employed.

This topic is closed to new replies.

Advertisement