Operating System: Three Easy Pieces --- Condition Variables (Note)

The other major component of any threads library, and certainly the case with POSIX threads, is

the presence of a condition variable. Condition variables are useful when some kind of signaling

must take place between threads, if one thread is waiting for another to do something before it

can continue. Two primary routines are used by programs wishing to interact in this way:

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 condition

. When calling either of the above routines this lock should be locked.

The first routine, pthread_cond_wait(), puts the calling thread to sleep, and thus waits for some 

other thread to signal it, usually when something in the program has changed that the now-

sleeping thread might care about. A typical usage looks like this:

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 the

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!

原文地址:https://www.cnblogs.com/miaoyong/p/4977316.html