Another common situation that occurs in multithreaded programs is the need for a thread to wait until something happens. This something could be anything! It could be the fact that data is now available from a device, or that a conveyor belt has now moved to the proper position, or that data has been committed to disk, or whatever. Another twist to throw in here is that several threads may need to wait for the given event.
To accomplish this, we'd use either a condition variable (which we'll see next) or the much simpler sleepon lock.
To use sleepon locks, you actually need to perform several operations. Let's look at the calls first, and then look at how you'd use the locks.
int pthread_sleepon_lock (void); int pthread_sleepon_unlock (void); int pthread_sleepon_broadcast (void *addr); int pthread_sleepon_signal (void *addr); int pthread_sleepon_wait (void *addr);
As described above, a thread needs to wait for something to happen. The most obvious choice in the list of functions above is the pthread_sleepon_wait(). But first, the thread needs to check if it really does have to wait. Let's set up an example. One thread is a producer thread that's getting data from some piece of hardware. The other thread is a consumer thread that's doing some form of processing on the data that just arrived. Let's look at the consumer first:
volatile int data_ready = 0; consumer () { while (1) { while (!data_ready) { // WAIT } // process data } }
The consumer is sitting in its main processing loop (the while (1)); it's going to do its job forever. The first thing it does is look at the data_ready flag. If this flag is a 0, it means there's no data ready. Therefore, the consumer should wait. Somehow, the producer will wake it up, at which point the consumer should reexamine its data_ready flag. Let's say that's exactly what happens, and the consumer looks at the flag and decides that it's a 1, meaning data is now available. The consumer goes off and processes the data, and then goes to see if there's more work to do, and so on.
We're going to run into a problem here. How does the consumer reset the data_ready flag in a synchronized manner with the producer? Obviously, we're going to need some form of exclusive access to the flag so that only one of those threads is modifying it at a given time. The method that's used in this case is built with a mutex, but it's a mutex that's buried in the implementation of the sleepon library, so we can access it only via two functions: pthread_sleepon_lock() and pthread_sleepon_unlock(). Let's modify our consumer:
consumer () { while (1) { if (pthread_sleepon_lock () == EOK) { while (!data_ready) { // WAIT } // process data data_ready = 0; pthread_sleepon_unlock (); } } }
Now we've added the lock and unlock around the operation of the consumer. This means that the consumer can now reliably test the data_ready flag, with no race conditions, and also reliably set the flag.
Okay, great. Now what about the WAIT call? As we suggested earlier, it's effectively the pthread_sleepon_wait() call. Here's the second while loop:
while (!data_ready) { pthread_sleepon_wait (&data_ready); }
The pthread_sleepon_wait() actually does three distinct steps!
The reason it has to unlock and lock the sleepon library's mutex is simple—since the whole idea of the mutex is to ensure mutual exclusion to the data_ready variable, this means that we want to lock out the producer from touching the data_ready variable while we're testing it. But, if we don't do the unlock part of the operation, the producer would never be able to set it to tell us that data is indeed available! The re-lock operation is done purely as a convenience; this way the user of the pthread_sleepon_wait() doesn't have to worry about the state of the lock when it wakes up.
Let's switch over to the producer side and see how it uses the sleepon library. Here's the full implementation:
producer () { while (1) { // wait for interrupt from hardware here... if (pthread_sleepon_lock () == EOK) { data_ready = 1; pthread_sleepon_signal (&data_ready); pthread_sleepon_unlock (); } } }
As you can see, the producer locks the mutex as well so that it can have exclusive access to the data_ready variable in order to set it.
We've identified the consumer and producer states as:
State | Meaning |
---|---|
CONDVAR | Waiting for the underlying condition variable associated with the sleepon |
MUTEX | Waiting for a mutex |
READY | Capable of using, or already using, the CPU |
INTERRUPT | Waiting for an interrupt from the hardware |
Let's examine in detail what happens:
Action | Mutex owner | Consumer state | Producer state |
---|---|---|---|
Consumer locks mutex | Consumer | READY | INTERRUPT |
Consumer examines data_ready | Consumer | READY | INTERRUPT |
Consumer calls pthread_sleepon_wait() | Consumer | READY | INTERRUPT |
pthread_sleepon_wait() unlocks mutex | Free | READY | INTERRUPT |
pthread_sleepon_wait() blocks | Free | CONDVAR | INTERRUPT |
Time passes | Free | CONDVAR | INTERRUPT |
Hardware generates data | Free | CONDVAR | READY |
Producer locks mutex | Producer | CONDVAR | READY |
Producer sets data_ready | Producer | CONDVAR | READY |
Producer calls pthread_sleepon_signal() | Producer | CONDVAR | READY |
Consumer wakes up, pthread_sleepon_wait() tries to lock mutex | Producer | MUTEX | READY |
Producer releases mutex | Free | MUTEX | READY |
Consumer gets mutex | Consumer | READY | READY |
Consumer processes data | Consumer | READY | READY |
Producer waits for more data | Consumer | READY | INTERRUPT |
Time passes (consumer processing) | Consumer | READY | INTERRUPT |
Consumer finishes processing, unlocks mutex | Free | READY | INTERRUPT |
Consumer loops back to top, locks mutex | Consumer | READY | INTERRUPT |
The last entry in the table is a repeat of the first entry—we've gone around one complete cycle.
What's the purpose of the data_ready variable? It actually serves two purposes:
We'll defer the discussion of What's the difference between pthread_sleepon_signal() and pthread_sleepon_broadcast() to the discussion of condition variables next.