The other major component of any threads library, and certainly the case with POSIX threads, islinux
the presence of a condition variable. Condition variables are useful when some kind of signalingapp
must take place between threads, if one thread is waiting for another to do something before itide
can continue. Two primary routines are used by programs wishing to interact in this way:oop
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex); int pthread_cond_signal(pthread_cond_t* cond);
To use a condition variable, one has to in addition have a lock that is associated with this conditionui
. When calling either of the above routines this lock should be locked.this
The first routine, pthread_cond_wait(), puts the calling thread to sleep, and thus waits for some idea
other thread to signal it, usually when something in the program has changed that the now-spa
sleeping thread might care about. A typical usage looks like this:rest
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; pthread_mutex_lock(&lock); while (ready == 0) { pthread_cond_wait(&cond, &wait); } pthread_mutex_unlock(&lock);
In this code, after initialization of the relevant lock and condition, a thread checks to see if thecode
variable ready has yet been set to something other than zero. If not, the thread simply calls the
wait routine in order to sleep until some other thread wakes it.
The code to wake a thread, which would run in some other thread, looks like this:
pthread_mutex_lock(&lock); ready = 1; pthread_cond_signal(&cond); pthread_mutex_unlock(&lock);
A few things to note about this code sequence. First, when signaling as well as when modifying
the global variable ready, we always make sure to have the lock held. This ensures that we do
not accidentally introduce a race condition into our code.
Second, you might notice that the wait call takea a lock as its second parameter, whereas signal
call only takes a condiiton. The reason for this difference is that the wait call, in addition to putting
the calling thread to sleep, release the lock when putting said caller to sleep. Imagine if it did not:
how could the other thread acquire the lock and signal it to wake up? However, before returning
after being woken, the pthread_cond_wait() reacquires the lock, thus ensuring that any time the
waiting thread is running between the lock acquire at the beginning of the wait sequence, and
the lock release at the end, it holds the lock.
On last oddity: the waiting thread re-checks the condition in a while loop, instead of a simple if
statement. We will discuss this issue in detail when we study condiiton variables in a future
chapter, but in general, using a while loop is the simple and safe thing to do. Although it rechecks
the condition perhaps adding a little overhead, there are some pthread implementations that
could spuriously wake up a waiting thread; in such a case, without rechecking, the waiting thread
will continue thinking that the condition has changed even though it has not. It is safer thus to
view waking up as a hint that something might have changed, rather than an absolute fact.
Note that sometines it is temping to use a simple flag to signal between two threads, instead of
a condition variable and associated lock. For example, we could rewrite the waiting code above to
look more like this in the waiting code:
while (ready == 0) ;
Don't ever do this, for the following reasons. First, it performas poorly in many cases spinning for
a long time just wastes CPU cycles. Second, it is error prone. As recent research shows, it is
surprisingly easy to make mistaks when using flags as above to synchronize between threads;
in that study, roughly half the uses of these ad hoc synchronizations were buggy! Don't be lazy;
use condition variables even when you think you can get away without doing so.
If condition variables sound confusing, don't worry too much yet, we will be covering them in
great detail in a subsequent chapter. Until then, it should suffice to know that they exist and to
have some idea how and why they are used.
Compiling and Running
All of the code examples in this chapter are relatively easy to get up and running. To compile them
, you must include the header pthread.h in your code. On the link line, you must also explicitly link
with the pthreads libraary, by adding the -pthread flag.
For example, to compile a simple multi-threaded program, all you have to do is the following:
prompt> gcc -o main main.c -Wall -pthread
As long as main.c includes the pthreads header, you have now successfully compiled a concurrent
program. Whether it works or not, as usual, is a different matter entirely.
We have introduced the basics of the pthread library, including thread creation, building mutual
exclusion via locks, and signaling and waiting via condition variables. You don't need much else
to write robust and efficient multi-threaded code, except patience and a great deal of care.
We now end the chapter with a set of tips that might be useful to you when you write multi-
threaded code see the aside on the following page for details. There are other aspects of the API
that are interesting; if you want some more information, type man -k pthread on a linux system
to see over a hundred APIs that make up the entire interface. However, the basics discussed
herein should enable you to build sophisticated and hopefully, correct and perfromant multi-
threaded programs. The hard part with threads is not the APIs, but rather the tricky logic of how
you build concurrent programs. Read on to learn more.
Aside: Thread API Guidelines
There are a number of small but important things to remember when use the POSIX thread library
or really, any thread library to build a multi-threaded program. They are:
keep it small. Above all else, any code to lock or signal between threads should be as simple as
possible. Tricky thread interactions lead to bugs.
Minimize thread interactions. Try to keep the number of ways in which threads interact to a minimum.
Each interaction should be carefully thought out and constructed with tried and true approaches many
of which we will learn about in the coming chapters.
Initialize locks and condition variables. Failure to do so will lead to code that sometimes works and
sometimes fails in very strange ways.
Check your return codes. Of course, in any C and UNIX programming you do, you should be checking
each and every return code, and it's true here as well. Failure to do so will lead to bizarre and hard to
understand behavior, making you likely to scream, pull some of your hair, or both.
Be careful with how you pass arguments to, and return values from, threads. In particular, any time
you are passing a reference to a variable allocated on the stack, you aer probably doing something
wrong.
Each thread has its stack. As related to the point above, please remember that each thread has its
own stack. Thus, if you have a locally-allocated variable inside of some function a thread is executing
, it is essentially private to that thread; no other thread can easily access it. To share data between
threads, the values must be in the heap or othrewise some locale that is globally accessible.
Always use condition variables to signal between threads. While it is often tempting to use simple
flag, don't do it.
Use the manual pages. On linux, in particular, the pthread man pages are highly informative and
discuss much of nuances presented here, often in even more detail, read them carefully!