Race Conditions

Race conditions occur when multiple threads access the same data, non-atomically modifying it, and the result depends on the order in which their operations are interleaved.

Recall that atomic means indivisible; a sequence of operations is atomic with respect to some data if that data cannot be accessed (read or written) by another thread during that sequence.

Synchronization Primitives

We'll discuss two concepts that are independent parts of synchronization:

  1. Mutual exclusion: Protection of some data from simultaneous access by multiple threads.
  2. Inter-thread scheduling: Communication between threads that some event happened; blocking for events in other threads.

General points about synchronization:

Locks/Mutexs

Thread Exit/Join

Semaphores

Monitors/Condition Variables

Condition Variables - Why?

So, why do we need these condition variable things anyway? We'll use a common example, passing data through an unlimited size buffer between two threads, and attempt to solve it without the condition variable. Like simplethreads, we are using Mesa-style condition variables.

We'll assume the following code is used to initialize the system appropriatly:

Initialization
sthread_mutex_t lock = sthread_mutex_init();
sthread_cond_t data_available = sthread_cond_init();
buffer = empty buffer;

The correct version, with condition variables. We assume that two threads are using the code, one calling AddToBuffer and another calling RemoveFromBuffer.

AddToBufferRemoveFromBuffer
sthread_mutex_lock(lock);
put item in buffer;
sthread_cond_signal(data_available);
sthread_mutex_unlock(lock);
sthread_mutex_lock(lock);
while (nothing in buffer) {
    sthread_cond_wait(data_available, lock);
}
remove item from buffer;
sthread_mutex_unlock(lock);
return item;

First, lets try just removing condition variable:

AddToBufferRemoveFromBuffer
sthread_mutex_lock(lock);
put item in buffer;
sthread_mutex_unlock(lock);
sthread_mutex_lock(lock);
while (nothing in buffer) {
    ;
}
remove item from buffer;
sthread_mutex_unlock(lock);
return item;

Obviously no good - the while loop holds the lock, meaning no way to run AddToBuffer, so while will run forever....

OK, so release the lock in the while loop (but remember to reacquire it before checking if anything is in the buffer):

AddToBufferRemoveFromBuffer
sthread_mutex_lock(lock);
put item in buffer;
sthread_mutex_unlock(lock);
sthread_mutex_lock(lock);
while (nothing in buffer) {
    sthread_mutex_unlock(lock);
    sthread_mutex_lock(lock);
}
remove item from buffer;
sthread_mutex_unlock(lock);
return item;

This code will work (surprisingly), but very slowly - if one thread enters RemoveFromBuffer (with an empty buffer) and then another thread enters AddToBuffer, the only way to make progress is to context switch out of RemoveFromBuffer in-between the two lock statements. That's no good.

What if we add a sthread_yield() to speed things up a bit - that way, at least the scheduler will have a better chance of context switching us there. (That the algorithm is correct, but very inefficient, is a good sign we have an inter-thread scheduling problem and not a mutual exclusion problem).

AddToBufferRemoveFromBuffer
sthread_mutex_lock(lock);
put item in buffer;
sthread_mutex_unlock(lock);
sthread_mutex_lock(lock);
while (nothing in buffer) {
    sthread_mutex_unlock(lock);
    sthread_yield();
    sthread_mutex_lock(lock);
}
remove item from buffer;
sthread_mutex_unlock(lock);
return item;

That is looking a little better, but a yield isn't quite right - it is quite likely we will have to go around the while loop many, many times before some thread actually adds anything to the buffer. What we really want is to sleep until there is data available, and have AddToBuffer wake us up.

Lets try and do that by adding a queue to hold waiting threads (call it waiters). AddToBuffer will take the first thread off of the queue and wake it up. We can then change the sthread_yield() to a sleep(3), since AddToBuffer should wake us up...

AddToBufferRemoveFromBuffer
sthread_mutex_lock(lock);
put item in buffer;
if (waiters not empty)
    waiters.remove_next().wakeup();

sthread_mutex_unlock(lock);
sthread_mutex_lock(lock);
while (nothing in buffer) {
1    sthread_mutex_unlock(lock);
2    waiters.enqueue(this thread);
3    sleep(); // sleep until wakeup is called.

4    sthread_mutex_lock(lock);
}
remove item from buffer;
sthread_mutex_unlock(lock);
return item;

OK, that looks good. Where is the problem? Consider a context switch from thread A between lines 1 and 2 in RemoveFromBuffer to thread B just entering AddToBuffer. Thread B would successfully add the item to the buffer, and then check if any waiters were available. Since thread A has not yet added itself to the waiters queue, it wouldn't find any, and it would return. At some point, thread A would run again, and would sleep - even though there is actually data available in the buffer.

What we need is an atomic version of lines 1 through 4. Hmm, that happens to be exactly what a condition variable does. Note that we were able to solve the locking/consistency part of the problem without condition variables, but to get the scheduling part right, we needed them.

You may also be wondering: When would we ever need more than 1 condition variable? Here is a quick example where we have a finite-size buffer, and add another CV, space_available, to handle the inter-thread scheduling constraints imposed by a buffer being full:

AddToBufferRemoveFromBuffer
sthread_mutex_lock(lock);
while (buffer full)
    sthread_cond_wait(space_available, lock);
put item in buffer;
sthread_cond_signal(data_available);
sthread_mutex_unlock(lock);
sthread_mutex_lock(lock);
while (nothing in buffer)
    sthread_cond_wait(data_available, lock);
remove item from buffer;
sthread_cond_signal(space_available)
sthread_mutex_unlock(lock);
return item;

Note that both CVs use the same lock; the lock is really to provide mutual exclusion to the buffer data structure, and since both conditions are related to that data structure, both CVs use the same lock.

Basically, anytime you have multiple different conditions, use another condition variable.

Valid XHTML 1.0!