POSIX supports mutex locks for short-term locking and condition variables for waiting on events of unbounded duration. Signal handling in threaded programs presents additional complications that can be reduced if signal handlers are replaced with dedicated threads. This chapter illustrates these thread synchronization concepts by implementing controlled access to shared objects, reader-writer synchronization and barriers.
This chapter discusses mutex locks, conditions variables and read-write locks. Table 13.1 summarizes the synchronization functions that are available in the POSIX:THR Extension. Each synchronization mechanism provides an initialization function and a function for destroying the object. The mutex locks and condition variables allow static initialization. All three types of synchronization have associated attribute objects, but we work only with synchronization objects that have the default attributes.
Table 13.1. Synchronization functions for POSIX:THR threads.
description | POSIX function |
---|---|
mutex locks |
|
| |
| |
| |
| |
condition variables |
|
| |
| |
| |
| |
| |
read-write locks |
|
| |
| |
| |
| |
| |
| |
|
A mutex is a special variable that can be either in the locked state or the unlocked state. If the mutex is locked, it has a distinguished thread that holds or owns the mutex. If no thread holds the mutex, we say the mutex is unlocked, free or available. The mutex also has a queue for the threads that are waiting to hold the mutex. The order in which the threads in the mutex queue obtain the mutex is determined by the thread-scheduling policy, but POSIX does not require that any particular policy be implemented.
When the mutex is free and a thread attempts to acquire the mutex, that thread obtains the mutex and is not blocked. It is convenient to think of this case as first causing the thread to enter the queue and then automatically removing it from the queue and giving it the mutex.
The mutex or mutex lock is the simplest and most efficient thread synchronization mechanism. Programs use mutex locks to preserve critical sections and to obtain exclusive access to resources. A mutex is meant to be held for short periods of time. Mutex functions are not thread cancellation points and are not interrupted by signals. A thread that waits for a mutex is not logically interruptible except by termination of the process, termination of a thread with pthread_exit
(from a signal handler), or asynchronous cancellation (which is normally not used).
Mutex locks are ideal for making changes to data structures in which the state of the data structure is temporarily inconsistent, as when updating pointers in a shared linked list. These locks are designed to be held for a short time. Use condition variables to synchronize on events of indefinite duration such as waiting for input.
POSIX uses variables of type pthread_mutex_t
to represent mutex locks. A program must always initialize pthread_mutex_t
variables before using them for synchronization. For statically allocated pthread_mutex_t
variables, simply assign PTHREAD_MUTEX_INITIALIZER
to the variable. For mutex variables that are dynamically allocated or that don’t have the default mutex attributes, call pthread_mutex_init
to perform initialization.
The mutex
parameter of pthread_mutex_init
is a pointer to the mutex to be initialized. Pass NULL
for the attr
parameter of pthread_mutex_init
to initialize a mutex with the default attributes. Otherwise, first create and initialize a mutex attribute object in a manner similar to that used for thread attribute objects.
SYNOPSIS #include <pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; POSIX:THR
If successful, pthread_mutex_init
returns 0. If unsuccessful, pthread_mutex_init
returns a nonzero error code. The following table lists the mandatory errors for pthread_mutex_init
.
error | cause |
---|---|
| system lacks nonmemory resources needed to initialize |
| system lacks memory resources needed to initialize |
| caller does not have appropriate privileges |
Example 13.1.
The following code segment initializes the mylock
mutex with the default attributes, using the static initializer.
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
The mylock
variable must be allocated statically.
Static initializers are usually more efficient than pthread_mutex_init
, and they are guaranteed to be performed exactly once before any thread begins execution.
Example 13.2.
The following code segment initializes the mylock
mutex with the default attributes. The mylock
variable must be accessible to all the threads that use it.
int error; pthread_mutex_t mylock; if (error = pthread_mutex_init(&mylock, NULL)) fprintf(stderr, "Failed to initialize mylock:%s ", strerror(error));
Example 13.2 uses the strerror
function to output a message associated with error
. Unfortunately, POSIX does not require strerror
to be thread-safe (though many implementations have made it thread-safe). If multiple threads don’t call strerror
at the same time, you can still use it in threaded programs. For example, if all functions return error indications and only the main thread prints error messages, the main thread can safely call strerror
. Section 13.7 gives a thread-safe and signal-safe implementation, strerror_r
.
The pthread_mutex_destroy
function destroys the mutex referenced by its parameter. The mutex
parameter is a pointer to the mutex to be destroyed. A pthread_mutex_t
variable that has been destroyed with pthread_mutex_destroy
can be reinitialized with pthread_mutex_init
.
SYNOPSIS #include <pthread.h> int pthread_mutex_destroy(pthread_mutex_t *mutex); POSIX:THR
If successful, pthread_mutex_destroy
returns 0. If unsuccessful, it returns a nonzero error code. No mandatory errors are defined for pthread_mutex_destroy
.
POSIX has two functions, pthread_mutex_lock
and pthread_mutex_trylock
for acquiring a mutex. The pthread_mutex_lock
function blocks until the mutex is available, while the pthread_mutex_trylock
always returns immediately. The pthread_mutex_unlock
function releases the specified mutex. All three functions take a single parameter, mutex
, a pointer to a mutex.
SYNOPSIS #include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); POSIX:THR
If successful, these functions return 0. If unsuccessful, these functions return a nonzero error code. The following table lists the mandatory errors for the three functions.
error | cause |
---|---|
| mutex has protocol attribute |
| another thread holds the lock ( |
The PTHREAD_PRIO_PROTECT
attribute prevents priority inversions of the sort described in Section 13.8.
Example 13.6.
The following code segment uses a mutex to protect a critical section.
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&mylock); /* critical section */ pthread_mutex_unlock(&mylock);
The code omits error checking for clarity.
Locking and unlocking are voluntary in the sense that a program achieves mutual exclusion only when its threads correctly acquire the appropriate mutex before entering their critical sections and release the mutex when finished. Nothing prevents an uncooperative thread from entering its critical section without acquiring the mutex. One way to ensure exclusive access to objects is to permit access only through well-defined functions and to put the locking calls in these functions. The locking mechanism is then transparent to the calling threads.
Program 13.1 shows an example of a thread-safe counter that might be used for reference counts in a threaded program. The locking mechanisms are hidden in the functions, and the calling program does not have to worry about using mutex variables. The count
and countlock
variables have the static
attribute, so these variables can be referenced only from within counter.c
. Following the pattern of the POSIX threads library, the functions in Program 13.1 return 0 if successful or a nonzero error code if unsuccessful.
Example 13.7.
What can go wrong in a threaded program if the count
variable of Program 13.1 is not protected with mutex locks?
Answer:
Without locking, it is possible to get an incorrect value for count
, since incrementing and decrementing a variable are not atomic operations on most machines. (Typically, incrementing consists of three distinct steps: loading a memory location into a CPU register, adding 1 to the register, and storing the value back in memory.) Suppose a thread is in the middle of the increment when the process quantum expires. The thread scheduler may select another thread to run when the process runs again. If the newly selected thread also tries to increment or decrement count
, the variable’s value will be incorrect when the original thread completes its operation.
Example 13.1. counter.c
A counter that can be accessed by multiple threads.
#include <pthread.h> static int count = 0; static pthread_mutex_t countlock = PTHREAD_MUTEX_INITIALIZER; int increment(void) { /* increment the counter */ int error; if (error = pthread_mutex_lock(&countlock)) return error; count++; return pthread_mutex_unlock(&countlock); } int decrement(void) { /* decrement the counter */ int error; if (error = pthread_mutex_lock(&countlock)) return error; count--; return pthread_mutex_unlock(&countlock); } int getcount(int *countp) { /* retrieve the counter */ int error; if (error = pthread_mutex_lock(&countlock)) return error; *countp = count; return pthread_mutex_unlock(&countlock); }
A mutex can be used to protect an unsafe library function. The rand
function from the C library takes no parameters and returns a pseudorandom integer in the range 0 to RAND_MAX
. It is listed in the POSIX standard as being unsafe in multithreaded applications. The rand
function can be used in a multithreaded environment if it is guaranteed that no two threads are concurrently calling it. Program 13.2 shows an implementation of the function randsafe
that uses rand
to produce a single per-process sequence of pseudorandom double values in the range from 0 to 1. Note that rand
and therefore randsafe
are not particularly good generators; avoid them in real applications.
Example 13.2. randsafe.c
A random number generator protected by a mutex.
#include <pthread.h> #include <stdlib.h> int randsafe(double *ranp) { static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; int error; if (error = pthread_mutex_lock(&lock)) return error; *ranp = (rand() + 0.5)/(RAND_MAX + 1.0); return pthread_mutex_unlock(&lock); }
Program 13.3 shows an implementation of a synchronized flag that is initially zero. The getdone
function returns the value of the synchronized flag, and the setdone
function changes the value of the synchronized flag to 1.
Example 13.3. doneflag.c
A synchronized flag that is 1 if setdone
has been called at least once.
#include <pthread.h> static int doneflag = 0; static pthread_mutex_t donelock = PTHREAD_MUTEX_INITIALIZER; int getdone(int *flag) { /* get the flag */ int error; if (error = pthread_mutex_lock(&donelock)) return error; *flag = doneflag; return pthread_mutex_unlock(&donelock); } int setdone(void) { /* set the flag */ int error; if (error = pthread_mutex_lock(&donelock)) return error; doneflag = 1; return pthread_mutex_unlock(&donelock); }
Example 13.8.
The following code segment uses the synchronized flag of Program 13.3 to decide whether to process another command in a threaded program.
void docommand(void); int error = 0; int done = 0; while(!done && !error) { docommand(); error = getdone(&done); }
Program 13.4 shows a synchronized implementation of a global error value. Functions from different files can call seterror
with return values from various functions. The seterror
function returns immediately if the error
parameter is zero, indicating no error. Otherwise, seterror
acquires the mutex and assigns error
to globalerror
if globalerror
is zero. In this way, globalerror
holds the error code of the first error that it is assigned. Notice that seterror
returns the original error unless there was a problem acquiring or releasing the internal mutex. In this case, the global error value may not be meaningful and both seterror
and geterror
return the error code from the locking problem.
Example 13.4. globalerror.c
A shared global error flag.
#include <pthread.h> static int globalerror = 0; static pthread_mutex_t errorlock = PTHREAD_MUTEX_INITIALIZER; int geterror(int *error) { /* get the error flag */ int terror; if (terror = pthread_mutex_lock(&errorlock)) return terror; *error = globalerror; return pthread_mutex_unlock(&errorlock); } int seterror(int error) { /* globalerror set to error if first error */ int terror; if (!error) /* it wasn't an error, so don't change globalerror */ return error; if (terror = pthread_mutex_lock(&errorlock)) /* couldn't get lock */ return terror; if (!globalerror) globalerror = error; terror = pthread_mutex_unlock(&errorlock); return terror? terror: error; }
Program 13.5 shows a synchronized implementation of a shared sum object that uses the global error flag of Program 13.4.
Example 13.5. sharedsum.c
A shared sum object that uses the global error flag of Program 13.4.
#include <pthread.h> #include "globalerror.h" static int count = 0; static double sum = 0.0; static pthread_mutex_t sumlock = PTHREAD_MUTEX_INITIALIZER; int add(double x) { /* add x to sum */ int error; if (error = pthread_mutex_lock(&sumlock)) return seterror(error); sum += x; count++; error = pthread_mutex_unlock(&sumlock); return seterror(error); } int getsum(double *sump) { /* return sum */ int error; if (error = pthread_mutex_lock(&sumlock)) return seterror(error); *sump = sum; error = pthread_mutex_unlock(&sumlock); return seterror(error); } int getcountandsum(int *countp, double *sump) { /* return count and sum */ int error; if (error = pthread_mutex_lock(&sumlock)) return seterror(error); *countp = count; *sump = sum; error = pthread_mutex_unlock(&sumlock); return seterror(error); }
Because mutex locks must be accessible to all the threads that need to synchronize, they often appear as global variables (internal or external linkage). Although C is not object oriented, an object organization is often useful. Internal linkage should be used for those objects that do not need to be accessed from outside a given file. Programs 13.1 through 13.5 illustrate methods of doing this. We now illustrate how to use these synchronized objects in a program.
Program 13.6 shows a function that can be called as a thread to do a simple calculation. The computethread
calculates the sine of a random number between 0 and 1 in a loop, adding the result to the synchronized sum given by Program 13.5. The computethread
sleeps for a short time after each calculation, allowing other threads to use the CPU. The computethread
thread uses the doneflag
of Program 13.3 to terminate when another thread sets the flag.
Example 13.6. computethread.c
A thread that computes sums of random sines.
#include <math.h> #include <stdlib.h> #include <time.h> #include "doneflag.h" #include "globalerror.h" #include "randsafe.h" #include "sharedsum.h" #define TEN_MILLION 10000000L /* ARGSUSED */ void *computethread(void *arg1) { /* compute a random partial sum */ int error; int localdone = 0; struct timespec sleeptime; double val; sleeptime.tv_sec = 0; sleeptime.tv_nsec = TEN_MILLION; /* 10 ms */ while (!localdone) { if (error = randsafe(&val)) /* get a random number between 0.0 and 1.0 */ break; if (error = add(sin(val))) break; if (error = getdone(&localdone)) break; nanosleep(&sleeptime, NULL); /* let other threads in */ } seterror(error); return NULL; }
Program 13.7 is a driver program that creates a number of computethread
threads and allows them to compute for a given number of seconds before it sets a flag to end the calculations. The main
program then calls the showresults
function of Program 13.8 to retrieve the shared sum and number of the summed values. The showresults
function computes the average from these values. It also calculates the theoretical average value of the sine function over the interval [0,1] and gives the total and percentage error of the average value.
The second command-line argument of computethreadmain
is the number of seconds to sleep after creating the threads. After sleeping, computethreadmain
calls setdone
, causing the threads to terminate. The computethreadmain
program then uses pthread_join
to wait for the threads to finish and calls showresults
. The showresults
function uses geterror
to check to see that all threads completed without reporting an error. If all is well, showresults
displays the results.
Example 13.7. computethreadmain.c
A main
program that creates a number of computethread
threads and allows them to execute for a given number of seconds.
#include <math.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "computethread.h" #include "doneflag.h" #include "globalerror.h" #include "sharedsum.h" int showresults(void); int main(int argc, char *argv[]) { int error; int i; int numthreads; int sleeptime; pthread_t *tids; if (argc != 3) { /* pass number threads and sleeptime on command line */ fprintf(stderr, "Usage: %s numthreads sleeptime ", argv[0]); return 1; } numthreads = atoi(argv[1]); /* allocate an array for the thread ids */ sleeptime = atoi(argv[2]); if ((tids = (pthread_t *)calloc(numthreads, sizeof(pthread_t))) == NULL) { perror("Failed to allocate space for thread IDs"); return 1; } for (i = 0; i < numthreads; i++) /* create numthreads computethreads */ if (error = pthread_create(tids + i, NULL, computethread, NULL)) { fprintf(stderr, "Failed to start thread %d:%s ", i, strerror(error)); return 1; } sleep(sleeptime); /* give them some time to compute */ if (error = setdone()) { /* tell the computethreads to quit */ fprintf(stderr, "Failed to set done:%s ", strerror(error)); return 1; } for (i = 0; i < numthreads; i++) /* make sure that they are all done */ if (error = pthread_join(tids[i], NULL)) { fprintf(stderr, "Failed to join thread %d:%s ", i, strerror(error)); return 1; } if (showresults()) return 1; return 0; }
Example 13.8. showresults.c
A function that displays the results of the computethread
calculations.
#include <math.h> #include <stdio.h> #include <string.h> #include "globalerror.h" #include "sharedsum.h" int showresults(void) { double average; double calculated; int count; double err; int error; int gerror; double perr; double sum; if (((error = getcountandsum(&count, &sum)) != 0) || ((error = geterror(&gerror)) != 0)) { /* get results */ fprintf(stderr, "Failed to get results: %s ", strerror(error)); return -1; } if (gerror) { /* an error occurred in compute thread computation */ fprintf(stderr, "Failed to compute sum: %s ", strerror(gerror)); return -1; } if (count == 0) printf("No values were summed. "); else { calculated = 1.0 - cos(1.0); average = sum/count; err = average - calculated; perr = 100.0*err/calculated; printf("The sum is %f and the count is %d ", sum, count); printf("The average is %f and error is %f or %f%% ", average, err, perr); } return 0; }
Most shared data structures in a threaded program must be protected with synchronization mechanisms to ensure correct results. Program 13.9 illustrates how to use a single mutex to make the list object of Program 2.7 thread-safe. The listlib.c
program should be included in the listlib_r.c
file. All the functions in listlib.c
should be qualified with the static
attribute so that they are not accessible outside the file. The list object functions of Program 2.7 return –1 and set errno
to report an error. The implementation of Program 13.9 preserves this handling of the errors. Since each thread has its own errno
, setting errno
in the listlib_r
functions is not a problem. The implementation just wraps each function in a pair of mutex calls. Most of the code is for properly handling errors that occur during the mutex calls.
Example 13.9. listlib_r.c
Wrapper functions to make the list object of Program 2.7 thread-safe.
#include <errno.h> #include <pthread.h> static pthread_mutex_t listlock = PTHREAD_MUTEX_INITIALIZER; int accessdata_r(void) { /* return nonnegative traversal key if successful */ int error; int key; if (error = pthread_mutex_lock(&listlock)) { /* no mutex, give up */ errno = error; return -1; } key = accessdata(); if (key == -1) { error = errno; pthread_mutex_unlock(&listlock); errno = error; return -1; } if (error = pthread_mutex_unlock(&listlock)) { errno = error; return -1; } return key; } int adddata_r(data_t data) { /* allocate a node on list to hold data */ int error; if (error = pthread_mutex_lock(&listlock)) { /* no mutex, give up */ errno = error; return -1; } if (adddata(data) == -1) { error = errno; pthread_mutex_unlock(&listlock); errno = error; return -1; } if (error = pthread_mutex_unlock(&listlock)) { errno = error; return -1; } return 0; } int getdata_r(int key, data_t *datap) { /* retrieve node by key */ int error; if (error = pthread_mutex_lock(&listlock)) { /* no mutex, give up */ errno = error; return -1; } if (getdata(key, datap) == -1) { error = errno; pthread_mutex_unlock(&listlock); errno = error; return -1; } if (error = pthread_mutex_unlock(&listlock)) { errno = error; return -1; } return 0; } int freekey_r(int key) { /* free the key */ int error; if (error = pthread_mutex_lock(&listlock)) { /* no mutex, give up */ errno = error; return -1; } if (freekey(key) == -1) { error = errno; pthread_mutex_unlock(&listlock); errno = error; return -1; } if (error = pthread_mutex_unlock(&listlock)) { errno = error; return -1; } return 0; }
The implementation of Program 13.9 uses a straight locking strategy that allows only one thread at a time to proceed. Section 13.6 revisits this problem with an implementation that allows multiple threads to execute the getdata
function at the same time by using reader-writer synchronization.
If a mutex isn’t statically initialized, the program must call pthread_mutex_init
before using any of the other mutex functions. For programs that have a well-defined initialization phase before they create additional threads, the main thread can perform this initialization. Not all problems fit this structure. Care must be taken to call pthread_mutex_init
before any thread accesses a mutex, but having each thread initialize the mutex doesn’t work either. The effect of calling pthread_mutex_init
for a mutex that has already been initialized is not defined.
The notion of single initialization is so important that POSIX provides the pthread_once
function to ensure these semantics. The once_control
parameter must be statically initialized with PTHREAD_ONCE_INIT
. The init_routine
is called the first time pthread_once
is called with a given once_control
, and init_routine
is not called on subsequent calls. When a thread returns from pthread_once
without error, the init_routine
has been completed by some thread.
SYNOPSIS #include <pthread.h> int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)); pthread_once_t once_control = PTHREAD_ONCE_INIT; POSIX:THR
If successful, pthread_once
returns 0. If unsuccessful, pthread_once
returns a nonzero error code. No mandatory errors are defined for pthread_once
.
Program 13.10 uses pthread_once
to implement an initialization function printinitmutex
. Notice that var
isn’t protected by a mutex because it will be changed only once by printinitonce
, and that modification occurs before any caller returns from printinitonce
.
Example 13.10. printinitonce.c
A function that uses pthread_once
to initialize a variable and print a statement at most once.
#include <pthread.h> #include <stdio.h> static pthread_once_t initonce = PTHREAD_ONCE_INIT; int var; static void initialization(void) { var = 1; printf("The variable was initialized to %d ", var); } int printinitonce(void) { /* call initialization at most once */ return pthread_once(&initonce, initialization); }
The initialization
function of printinitonce
has no parameters, making it hard to initialize var
to something other than a fixed value. Program 13.11 shows an alternative implementation of at-most-once initialization that uses a statically initialized mutex. The printinitmutex
function performs the initialization and printing at most once regardless of how many different variables or values are passed. If successful, printinitmutex
returns 0. If unsuccessful, printinitmutex
returns a nonzero error code. The mutex in printinitmutex
is declared in the function so that it is accessible only inside the function. Giving the mutex static storage class guarantees that the same mutex is used every time the function is called.
Example 13.11. printinitmutex.c
A function that uses a statically initialized mutex to initialize a variable and print a statement at most once.
#include <pthread.h> #include <stdio.h> int printinitmutex(int *var, int value) { static int done = 0; static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; int error; if (error = pthread_mutex_lock(&lock)) return error; if (!done) { *var = value; printf("The variable was initialized to %d ", value); done = 1; } return pthread_mutex_unlock(&lock); }
Example 13.9.
The following code segment initializes whichiteration
to the index of the first loop iteration in which dostuff
returns a nonzero value.
int whichiteration = -1; void *thisthread(void *) { int i; for (i = 0; i < 100; i++) if (dostuff()) printinitmutex(&whichiteration, i); }
The whichiteration
value is changed at most once, even if the program creates several threads running thisthread
.
The testandsetonce
function of Program 13.12 atomically sets an internal variable to 1 and returns the previous value of the internal variable in its ovalue
parameter. The first call to testandsetonce
initializes done
to 0, sets *ovalue
to 0 and sets done
to 1. Subsequent calls set *ovalue
to 1. The mutex ensures that no two threads have ovalue
set to 0. If successful, testandsetonce
returns 0. If unsuccessful, testandsetonce
returns a nonzero error code.
Example 13.10.
What happens if you remove the static
qualifier from the done
and lock
variables of testandsetonce
of Program 13.12?
Answer:
The static
qualifier for variables inside a block ensures that they remain in existence for subsequent executions of the block. Without the static
qualifier, done
and lock
become automatic variables. In this case, each call to testandsetonce
allocates new variables and each return
deallocates them. The function no longer works.
Example 13.12. testandsetonce.c
A function that uses a mutex to set a variable to 1 at most once.
#include <pthread.h> int testandsetonce(int *ovalue) { static int done = 0; static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; int error; if (error = pthread_mutex_lock(&lock)) return error; *ovalue = done; done = 1; return pthread_mutex_unlock(&lock); }
Example 13.11.
Does testandsetonce
still work if you move the declarations of done
and lock
outside the testandsetonce
function?
Answer:
Yes, testandsetonce
still works. However, now done
and lock
are accessible to other functions defined in the same file. Keeping them inside the function is safer for enforcing at-most-once semantics.
Example 13.12.
Does the following use of testandsetonce
of Program 13.12 ensure that the initialization of var
and the printing of the message occur at most once?
int error; int oflag; int var; error = testandsetonce(&oflag); if (!error && !oflag) { var = 1; printf("The variable has been initialized to 1 "); } var++;
Answer:
No. Successive calls to testandsetonce
of Program 13.12 can return before the variable has been initialized. Consider the following scenario in which var
must be initialized before being incremented.
Thread A calls testandsetonce
.
The testandsetonce
returns in thread A.
Thread A loses the CPU.
Thread B calls testandsetonce
.
The executeonce
returns to thread B without printing or initializing var
.
Thread B assumes that var
has been initialized, and it increments the variable.
Thread A gets the CPU again and initializes var
to 1.
In this case, var
should have the value 2 since it was initialized to 1 and incremented once. Unfortunately, it has the value 1.
The strategies discussed in this section guarantee at-most-once execution. They do not guarantee that code has been executed at least once. At-least-once semantics are important for initialization. For example, suppose that you choose to use pthread_mutex_init
rather than the static initializer to initialize a mutex. You need both at-most-once and at-least-once semantics. In other words, you need to perform an operation such as initialization exactly once. Sometimes the structure of the program ensures that this is the case—a main thread performs all necessary initialization before creating any threads. In other situations, each thread must call initialization when it starts executing, or each function must call the initialization before accessing the mutex. In these cases, you will need to use at-most-once strategies in conjunction with the calls.
Consider the problem of having a thread wait until some arbitrary condition is satisfied. For concreteness, assume that two variables, x
and y
, are shared by multiple threads. We want a thread to wait until x
and y
are equal. A typical incorrect busy-waiting solution is
while (x != y) ;
Having a thread use busy waiting is particularly troublesome. Depending on how the threads are scheduled, the thread doing the busy waiting may prevent other threads from ever using the CPU, in which case x
and y
never change. Also, access to shared variables should always be protected.
Here is the correct strategy for non-busy waiting for the predicate x==y
to become true.
Lock a mutex.
Test the condition x==y
.
If true, unlock the mutex and exit the loop.
If false, suspend the thread and unlock the mutex.
The mutex must be held until a test determines whether to suspend the thread. Holding the mutex prevents the condition x==y
from changing between the test and the suspension of the thread. The mutex needs to be unlocked while the thread is suspended so that other threads can access x
and y
. The strategy assumes that the code protects all other access to the shared variables x
and y
with the mutex.
Applications manipulate mutex queues through well-defined system library functions such as pthread_mutex_lock
and pthread_mutex_unlock
. These functions are not sufficient to implement (in a simple manner) the queue manipulations required here. We need a new data type, one associated with a queue of processes waiting for an arbitrary condition such as x==y
to become true. Such a data type is called a condition variable.
A classical condition variable is associated with a particular condition. In contrast, POSIX condition variables provide an atomic waiting mechanism but are not associated with particular conditions.
The function pthread_cond_wait
takes a condition variable and a mutex as parameters. It atomically suspends the calling thread and unlocks the mutex. It can be thought of as placing the thread in a queue of threads waiting to be notified of a change in a condition. The function returns with the mutex reacquired when the thread receives a notification. The thread must test the condition again before proceeding.
Example 13.13.
The following code segment illustrates how to wait for the condition x==y
, using a POSIX condition variable v
and a mutex m
.
pthread_mutex_lock(&m); while (x != y) pthread_cond_wait(&v, &m); /* modify x or y if necessary */ pthread_mutex_unlock(&m);
When the thread returns from pthread_cond_wait
it owns m
, so it can safely test the condition again. The code segment omits error checking for clarity.
The function pthread_cond_wait
should be called only by a thread that owns the mutex, and the thread owns the mutex again when the function returns. The suspended thread has the illusion of uninterrupted mutex ownership because it owns the mutex before the call to pthread_cond_wait
and owns the mutex when pthread_cond_wait
returns. In reality, the mutex can be acquired by other threads during the suspension.
A thread that modifies x
or y
can call pthread_cond_signal
to notify other threads of the change. The pthread_cond_signal
function takes a condition variable as a parameter and attempts to wake up at least one of the threads waiting in the corresponding queue. Since the blocked thread cannot return from pthread_cond_wait
without owning the mutex, pthread_cond_signal
has the effect of moving the thread from the condition variable queue to the mutex queue.
Example 13.14.
The following code might be used by another thread in conjunction with Example 13.13 to notify the waiting thread that it has incremented x
.
pthread_mutex_lock(&m); x++; pthread_cond_signal(&v); pthread_mutex_unlock(&m);
The code segment omits error checking for clarity.
In Example 13.14, the caller holds the mutex while calling pthread_cond_signal
. POSIX does not require this to be the case, and the caller could have unlocked the mutex before signaling. In programs that have threads of different priorities, holding the mutex while signaling can prevent lower priority threads from acquiring the mutex and executing before a higher-priority thread is awakened.
Several threads may use the same condition variables to wait on different predicates. The waiting threads must verify that the predicate is satisfied when they return from the wait. The threads that modify x
or y
do not need to know what conditions are being waited for; they just need to know which condition variable is being used.
Example 13.15.
Compare the use of condition variables with the use of sigsuspend
as described in Example 8.24 on page 275.
Answer:
The concepts are similar. Example 8.24 blocks the signal and tests the condition. Blocking the signal is analogous to locking the mutex since the signal handler cannot access the global variable sigreceived
while the signal is blocked. The sigsuspend
atomically unblocks the signal and suspends the process. When sigsuspend
returns, the signal is blocked again. With condition variables, the thread locks the mutex to protect its critical section and tests the condition. The pthread_cond_wait
atomically releases the mutex and suspends the process. When pthread_cond_wait
returns, the thread owns the mutex again.
POSIX represents condition variables by variables of type pthread_cond_t
. A program must always initialize pthread_cond_t
variables before using them. For statically allocated pthread_cond_t
variables with the default attributes, simply assign PTHREAD_COND_INITIALIZER
to the variable. For variables that are dynamically allocated or don’t have the default attributes, call pthread_cond_init
to perform initialization. Pass NULL
for the attr
parameter of pthread_cond_init
to initialize a condition variable with the default attributes. Otherwise, first create and initialize a condition variable attribute object in a manner similar to that used for thread attribute objects.
SYNOPSIS #include <pthread.h> int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); pthread_cont_t cond = PTHREAD_COND_INITIALIZER; POSIX:THR
If successful, pthread_cond_init
returns 0. If unsuccessful, pthread_cond_init
returns a nonzero error code. The following table lists the mandatory errors for pthread_cond_init
.
error | cause |
---|---|
| system lacked nonmemory resources needed to initialize |
| system lacked memory resources needed to initialize |
Example 13.16.
The following code segment initializes a condition variable.
pthread_cond_t barrier; int error; if (error = pthread_cond_init(&barrier, NULL)); fprintf(stderr, "Failed to initialize barrier:%s ", strerror(error));
The code assumes that strerror
will not be called by multiple threads. Otherwise, strerror_r
of Section 13.7 should be used.
Example 13.17.
What happens if a thread tries to initialize a condition variable that has already been initialized?
Answer:
The POSIX standard explicitly states that the results are not defined, so you should avoid doing this.
The pthread_cond_destroy
function destroys the condition variable referenced by its cond
parameter. A pthread_cond_t
variable that has been destroyed with pthread_cond_destroy
can be reinitialized with pthread_cond_init
.
SYNOPSIS #include <pthread.h> int pthread_cond_destroy(pthread_cond_t *cond); POSIX:THR
If successful, pthread_cond_destroy
returns 0. If unsuccessful, it returns a nonzero error code. No mandatory errors are defined for pthread_cond_destroy
.
Condition variables derive their name from the fact that they are called in conjunction with testing a predicate or condition. Typically, a thread tests a predicate and calls pthread_cond_wait
if the test fails. The pthread_cond_timedwait
function can be used to wait for a limited time. The first parameter of these functions is cond
, a pointer to the condition variable. The second parameter is mutex
, a pointer to a mutex that the thread acquired before the call. The wait operation causes the thread to release this mutex when the thread is placed on the condition variable wait queue. The pthread_cond_timedwait
function has a third parameter, a pointer to the time to return if a condition variable signal does not occur first. Notice that this value represents an absolute time, not a time interval.
SYNOPSIS #include <pthread.h> int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); POSIX:THR
If successful, pthread_cond_timedwait
and pthread_cond_wait
return 0. If unsuccessful, these functions return nonzero error code. The pthread_cond_timedwait
function returns ETIMEDOUT
if the time specified by abstime
has expired. If a signal is delivered while a thread is waiting for a condition variable, these functions may resume waiting upon return from the signal handler, or they may return 0 because of a spurious wakeup.
Example 13.20.
The following code segment causes a thread to (nonbusy) wait until a
is greater than or equal to b
.
pthread_mutex_lock(&mutex); while (a < b) pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex);
The code omits error checking for clarity.
The calling thread should obtain a mutex before it tests the predicate or calls pthread_cond_wait
. The implementation guarantees that pthread_cond_wait
causes the thread to atomically release the mutex and block.
Example 13.21.
What happens if one thread executes the code of Example 13.20 by using mutex
and another thread executes Example 13.20 by using mutexA
?
Answer:
This is allowed as long as the two threads are not concurrent. The condition variable wait operations pthread_cond_wait
and pthread_cond_timedwait
effectively bind the condition variable to the specified mutex and release the binding on return. POSIX does not define what happens if threads use different mutex locks for concurrent wait operations on the same condition variable. The safest way to avoid this situation is to always use the same mutex with a given condition variable.
When another thread changes variables that might make the predicate true, it should awaken one or more threads that are waiting for the predicate to become true. The pthread_cond_signal
function unblocks at least one of the threads that are blocked on the condition variable pointed to by cond
. The pthread_cond_broadcast
function unblocks all threads blocked on the condition variable pointed to by cond
.
SYNOPSIS #include <pthread.h> int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond); POSIX:THR
If successful, pthread_condition_broadcast
and pthread_condition_signal
return 0. If unsuccessful, these functions return a nonzero error code.
Example 13.22.
Suppose v
is a condition variable and m
is a mutex. The following is a proper use of the condition variable to access a resource if the predicate defined by test_condition()
is true. This code omits error checking for clarity.
static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t v = PTHREAD_COND_INITIALIZER; pthread_mutex_lock(&m); while (!test_condition()) /* get resource */ pthread_cond_wait(&v, &m); /* do critical section, possibly changing test_condition() */ pthread_cond_signal(&v); /* inform another thread */ pthread_mutex_unlock(&m); /* do other stuff */
When a thread executes the pthread_cond_wait
in Example 13.22, it is holding the mutex m
. It blocks atomically and releases the mutex, permitting another thread to acquire the mutex and modify the variables in the predicate. When a thread returns successfully from a pthread_cond_wait
, it has acquired the mutex and can retest the predicate without explicitly reacquiring the mutex. Even if the program signals on a particular condition variable only when a certain predicate is true, waiting threads must still retest the predicate. The POSIX standard specifically allows pthread_cond_wait
to return, even if no thread has called pthread_cond_signal
or pthread_cond_broadcast
.
Program 6.2 on page 187 implements a simple barrier by using a pipe. Program 13.13 implements a thread-safe barrier by using condition variables. The limit
variable specifies how many threads must arrive at the barrier (execute the waitbarrier
) before the threads are released from the barrier. The count
variable specifies how many threads are currently waiting at the barrier. Both variables are declared with the static
attribute to force access through initbarrier
and waitbarrier
. If successful, the initbarrier
and waitbarrier
functions return 0. If unsuccessful, these functions return a nonzero error code.
Remember that condition variables are not linked to particular predicates and that pthread_cond_wait
can return because of spurious wakeups. Here are some rules for using condition variables.
Acquire the mutex before testing the predicate.
Retest the predicate after returning from a pthread_cond_wait
, since the return might have been caused by some unrelated event or by a pthread_cond_signal
that did not cause the predicate to become true.
Acquire the mutex before changing any of the variables appearing in the predicate.
Hold the mutex only for a short period of time—usually while testing the predicate or modifying shared variables.
Release the mutex either explicitly (with pthread_mutex_unlock
) or implicitly (with pthread_cond_wait
).
Example 13.13. tbarrier.c
Implementation of a thread-safe barrier.
#include <errno.h> #include <pthread.h> static pthread_cond_t bcond = PTHREAD_COND_INITIALIZER; static pthread_mutex_t bmutex = PTHREAD_MUTEX_INITIALIZER; static int count = 0; static int limit = 0; int initbarrier(int n) { /* initialize the barrier to be size n */ int error; if (error = pthread_mutex_lock(&bmutex)) /* couldn't lock, give up */ return error; if (limit != 0) { /* barrier can only be initialized once */ pthread_mutex_unlock(&bmutex); return EINVAL; } limit = n; return pthread_mutex_unlock(&bmutex); } int waitbarrier(void) { /* wait at the barrier until all n threads arrive */ int berror = 0; int error; if (error = pthread_mutex_lock(&bmutex)) /* couldn't lock, give up */ return error; if (limit <= 0) { /* make sure barrier initialized */ pthread_mutex_unlock(&bmutex); return EINVAL; } count++; while ((count < limit) && !berror) berror = pthread_cond_wait(&bcond, &bmutex); if (!berror) berror = pthread_cond_broadcast(&bcond); /* wake up everyone */ error = pthread_mutex_unlock(&bmutex); if (berror) return berror; return error; }
All threads in a process share the process signal handlers, but each thread has its own signal mask. The interaction of threads with signals involves several complications because threads can operate asynchronously with signals. Table 13.2 summarizes the three types of signals and their corresponding methods of delivery.
Table 13.2. Signal delivery in threads.
type | delivery action |
---|---|
asynchronous | delivered to some thread that has it unblocked |
synchronous | delivered to the thread that caused it |
directed | delivered to the identified thread ( |
Signals such as SIGFPE
(floating-point exception) are synchronous to the thread that caused them (i.e., they are always generated at the same point in the thread’s execution). Other signals are asynchronous because they are not generated at a predictable time nor are they associated with a particular thread. If several threads have an asynchronous signal unblocked, the thread runtime system selects one of them to handle the signal. Signals can also be directed to a particular thread with pthread_kill
.
The pthread_kill
function requests that signal number sig
be generated and delivered to the thread specified by thread
.
SYNOPSIS #include <signal.h> #include <pthread.h> int pthread_kill(pthread_t thread, int sig); POSIX:THR
If successful, pthread_kill
returns 0. If unsuccessful, pthread_kill
returns a nonzero error code. In the latter case, no signal is sent. The following table lists the mandatory errors for pthread_kill
.
error | cause |
---|---|
|
|
| no thread corresponds to specified ID |
Example 13.23.
The following code segment causes a thread to kill itself and the entire process.
if (pthread_kill(pthread_self(), SIGKILL)) fprintf(stderr, "Failed to commit suicide ");
Example 13.23 illustrates an important point regarding pthread_kill
. Although pthread_kill
delivers the signal to a particular thread, the action of handling it may affect the entire process. A common confusion is to assume that pthread_kill
always causes process termination, but this is not the case. The pthread_kill
just causes a signal to be generated for the thread. Example 13.23 causes process termination because the SIGKILL
signal cannot be caught, blocked or ignored. The same result occurs for any signal whose default action is to terminate the process unless the process ignores, blocks or catches the signal. Table 8.1 lists the POSIX signals with their symbolic names and default actions.
While signal handlers are process-wide, each thread has its own signal mask. A thread can examine or set its signal mask with the pthread_sigmask
function, which is a generalization of sigprocmask
to threaded programs. The sigprocmask
function should not be used when the process has multiple threads, but it can be called by the main thread before additional threads are created. Recall that the signal mask specifies which signals are to be blocked (not delivered). The how
and set
parameters specify the way the signal mask is to be modified, as discussed below. If the oset
parameter is not NULL
, the pthread_sigmask
function sets *oset
to the thread’s previous signal mask.
SYNOPSIS #include <pthread.h> #include <signal.h> int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset); POSIX:THR
If successful, pthread_sigmask
returns 0. If unsuccessful, pthread_sigmask
returns a nonzero error code. The pthread_sigmask
function returns EINVAL
if how
is not valid.
A how
value of SIG_SETMASK
causes the thread’s signal mask to be replaced by set
. That is, the thread now blocks all signals in set
but does not block any others. A how
value of SIG_BLOCK
causes the additional signals in set
to be blocked by the thread (added to the thread’s current signal mask). A how
value of SIG_UNBLOCK
causes any of the signals in set
that are currently being blocked to be removed from the thread’s current signal mask (no longer be blocked).
Signal handlers are process-wide and are installed with calls to sigaction
as in single-threaded processes. The distinction between process-wide signal handlers and thread-specific signal masks is important in threaded programs.
Recall from Chapter 8 that when a signal is caught, the signal that caused the event is automatically blocked on entry to the signal handler. With a multithreaded application, nothing prevents another signal of the same type from being delivered to another thread that has the signal unblocked. It is possible to have multiple threads executing within the same signal handler.
A recommended strategy for dealing with signals in multithreaded processes is to dedicate particular threads to signal handling. The main thread blocks all signals before creating the threads. The signal mask is inherited from the creating thread, so all threads have the signal blocked. The thread dedicated to handling the signal then executes sigwait
on that signal. (See Section 8.5.) Alternatively, the thread can use pthread_sigmask
to unblock the signal. The advantage of using sigwait
is that the thread is not restricted to async-signal-safe functions.
Program 13.14 is an implementation of a dedicated thread that uses sigwait
to handle a particular signal. A program calls signalthreadinit
to block the signo
signal and to create a dedicated signalthread
that waits for this signal. When the signal corresponding to signo
becomes pending, sigwait
returns and the signalthread
calls setdone
of Program 13.3 and returns. You can replace the setdone
with any thread-safe function. Program 13.14 has some informative messages, which would normally be removed.
Notice that the implementation of signalthreadinit
uses a thread attribute object to create signalthread
with higher priority than the default value. The program was tested on a system that used preemptive priority scheduling. When the program executes on this system without first increasing signalthread
’s priority, it still works correctly, but sometimes the program takes several seconds to react to the signal after it is generated. If a round-robin scheduling policy were available, all the threads could have the same priority.
The dedicated signal-handling thread, signalthread
, displays its priority to confirm that the priority is set correctly and then calls sigwait
. No signal handler is needed since sigwait
removes the signal from those pending. The signal is always blocked, so the default action for signalnum
is never taken.
Program 13.15 modifies computethreadmain
of Program 13.7 by using the SIGUSR1
signal to set the done flag for the computethread
object of Program 13.6. The main
program no longer sleeps a specified number of seconds before calling setdone
. Instead, the delivery of a SIGUSR1
signal causes signalthread
to call setdone
.
Example 13.14. signalthread.c
A dedicated thread that sets a flag when a signal is received.
#include <errno.h> #include <pthread.h> #include <signal.h> #include <stdio.h> #include "doneflag.h" #include "globalerror.h" static int signalnum = 0; /* ARGSUSED */ static void *signalthread(void *arg) { /* dedicated to handling signalnum */ int error; sigset_t intmask; struct sched_param param; int policy; int sig; if (error = pthread_getschedparam(pthread_self(), &policy, ¶m)) { seterror(error); return NULL; } fprintf(stderr, "Signal thread entered with policy %d and priority %d ", policy, param.sched_priority); if ((sigemptyset(&intmask) == -1) || (sigaddset(&intmask, signalnum) == -1) || (sigwait(&intmask, &sig) == -1)) seterror(errno); else seterror(setdone()); return NULL; } int signalthreadinit(int signo) { int error; pthread_attr_t highprio; struct sched_param param; int policy; sigset_t set; pthread_t sighandid; signalnum = signo; /* block the signal */ if ((sigemptyset(&set) == -1) || (sigaddset(&set, signalnum) == -1) || (sigprocmask(SIG_BLOCK, &set, NULL) == -1)) return errno; if ( (error = pthread_attr_init(&highprio)) || /* with higher priority */ (error = pthread_attr_getschedparam(&highprio, ¶m)) || (error = pthread_attr_getschedpolicy(&highprio, &policy)) ) return error; if (param.sched_priority < sched_get_priority_max(policy)) { param.sched_priority++; if (error = pthread_attr_setschedparam(&highprio, ¶m)) return error; } else fprintf(stderr, "Warning, cannot increase priority of signal thread. "); if (error = pthread_create(&sighandid, &highprio, signalthread, NULL)) return error; return 0; }
Example 13.15. computethreadsig.c
A main
program that uses signalthread
with the SIGUSR1
signal to terminate the computethread
computation of Program 13.6.
#include <math.h> #include <pthread.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "computethread.h" #include "globalerror.h" #include "sharedsum.h" #include "signalthread.h" int showresults(void); int main(int argc, char *argv[]) { int error; int i; int numthreads; pthread_t *tids; if (argc != 2) { /* pass number threads on command line */ fprintf(stderr, "Usage: %s numthreads ", argv[0]); return 1; } if (error = signalthreadinit(SIGUSR1)) { /* set up signal thread */ fprintf(stderr, "Failed to set up signal thread: %s ", strerror(error)); return 1; } numthreads = atoi(argv[1]); if ((tids = (pthread_t *)calloc(numthreads, sizeof(pthread_t))) == NULL) { perror("Failed to allocate space for thread IDs"); return 1; } for (i = 0; i < numthreads; i++) /* create numthreads computethreads */ if (error = pthread_create(tids+ i, NULL, computethread, NULL)) { fprintf(stderr, "Failed to start thread %d: %s ", i, strerror(error)); return 1; } fprintf(stderr, "Send SIGUSR1(%d) signal to proc %ld to stop calculation ", SIGUSR1, (long)getpid()); for (i = 0; i < numthreads; i++) /* wait for computethreads to be done */ if (error = pthread_join(tids[i], NULL)) { fprintf(stderr, "Failed to join thread %d: %s ", i, strerror(error)); return 1; } if (showresults()) return 1; return 0; }
The modular design of the signalthread
object makes the object easy to modify. Chapter 16 uses signalthread
for some implementations of a bounded buffer.
Example 13.24.
Run computethreadsig
of Program 13.15 from one command window. Send the SIGUSR1
signal from another command window, using the kill
shell command. What is its effect?
Answer:
The dedicated signal thread calls setdone
when the signal is pending, and the threads terminate normally.
The reader-writer problem refers to a situation in which a resource allows two types of access (reading and writing). One type of access must be granted exclusively (e.g., writing), but the other type may be shared (e.g., reading). For example, any number of processes can read from the same file without difficulty, but only one process should modify the file at a time.
Two common strategies for handling reader-writer synchronization are called strong reader synchronization and strong writer synchronization. Strong reader synchronization always gives preference to readers, granting access to readers as long as a writer is not currently writing. Strong writer synchronization always gives preference to writers, delaying readers until all waiting or active writers complete. An airline reservation system would use strong writer preference, since readers need the most up-to-date information. On the other hand, a library reference database might want to give readers preference.
POSIX provides read-write locks that allow multiple readers to acquire a lock, provided that a writer does not hold the lock. POSIX states that it is up to the implementation whether to allow a reader to acquire a lock if writers are blocked on the lock.
POSIX read-write locks are represented by variables of type pthread_rwlock_t
. Programs must initialize pthread_rwlock_t
variables before using them for synchronization by calling pthread_rwlock_init
. The rwlock
parameter is a pointer to a read-write lock. Pass NULL
for the attr
parameter of pthread_rwlock_init
to initialize a read-write lock with the default attributes. Otherwise, first create and initialize a read-write lock attribute object in a manner similar to that used for thread attribute objects.
SYNOPSIS #include <pthread.h> int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); POSIX:THR
If successful, pthread_rwlock_init
returns 0. If unsuccessful, it returns a nonzero error code. The following table lists the mandatory errors for pthread_rwlock_init
.
error | cause |
---|---|
| system lacked nonmemory resources needed to initialize |
| system lacked memory resources needed to initialize |
| caller does not have appropriate privileges |
Example 13.25.
What happens when you try to initialize a read-write lock that has already been initialized?
Answer:
POSIX states that the behavior under these circumstances is not defined.
The pthread_rwlock_destroy
function destroys the read-write lock referenced by its parameter. The rwlock
parameter is a pointer to a read-write lock. A pthread_rwlock_t
variable that has been destroyed with pthread_rwlock_destroy
can be reinitialized with pthread_rwlock_init
.
SYNOPSIS #include <pthread.h> int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); POSIX:THR
If successful, pthread_rwlock_destroy
returns 0. If unsuccessful, it returns a nonzero error code. No mandatory errors are defined for pthread_rwlock_destroy
.
Example 13.26.
What happens if you reference a read-write lock that has been destroyed?
Answer:
POSIX states that the behavior under these circumstances is not defined.
The pthread_rwlock_rdlock
and pthread_rwlock_tryrdlock
functions allow a thread to acquire a read-write lock for reading. The pthread_rwlock_wrlock
and pthread_rwlock_trywrlock
functions allow a thread to acquire a read-write lock for writing. The pthread_rwlock_rdlock
and pthread_rwlock_wrlock
functions block until the lock is available, whereas pthread_rwlock_tryrdlock
and pthread_rwlock_trywrlock
return immediately. The pthread_rwlock_unlock
function causes the lock to be released. These functions require that a pointer to the lock be passed as a parameter.
SYNOPSIS #include <pthread.h> int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); POSIX:THR
If successful, these functions return 0. If unsuccessful, these functions return a nonzero error code. The pthread_rwlock_tryrdlock
and pthread_rwlock_trywrlock
functions return EBUSY
if the lock could not be acquired because it was already held.
Example 13.27.
What happens if a thread calls pthread_rwlock_rdlock
on a lock that it has already acquired with pthread_rwlock_wrlock
?
Answer:
POSIX states that a deadlock may occur. (Implementations are free to detect a deadlock and return an error, but they are not required to do so.)
Example 13.28.
What happens if a thread calls pthread_rwlock_rdlock
on a lock that it has already acquired with pthread_rwlock_rdlock
?
Answer:
A thread may hold multiple concurrent read locks on the same read-write lock. It should make sure to match the number of unlock calls with the number of lock calls to release the lock.
Program 13.16 uses read-write locks to implement a thread-safe wrapper for the list object of Program 2.7. The listlib.c
module should be included in this file, and its functions should be qualified with the static
attribute. Program 13.16 includes an initialize_r
function to initialize the read-write lock, since no static initialization is available. This function uses pthread_once
to make sure that the read-write lock is initialized only one time.
Example 13.29.
Compare Program 13.16 to the thread-safe implementation of Program 13.9 that uses mutex locks. What are the advantages/disadvantages of each?
Answer:
The mutex is a low-overhead synchronization mechanism. Since each of the functions in Program 13.9 holds the listlock
only for a short period of time, Program 13.9 is relatively efficient. Because read-write locks have some overhead, their advantage comes when the actual read operations take a considerable amount of time (such as incurred by accessing a disk). In such a case, the strictly serial execution order would be inefficient.
Example 13.16. listlibrw_r.c
The list object of Program 2.7 synchronized with read-write locks.
#include <errno.h> #include <pthread.h> static pthread_rwlock_t listlock; static int lockiniterror = 0; static pthread_once_t lockisinitialized = PTHREAD_ONCE_INIT; static void ilock(void) { lockiniterror = pthread_rwlock_init(&listlock, NULL); } int initialize_r(void) { /* must be called at least once before using list */ if (pthread_once(&lockisinitialized, ilock)) lockiniterror = EINVAL; return lockiniterror; } int accessdata_r(void) { /* get a nonnegative key if successful */ int error; int errorkey = 0; int key; if (error = pthread_rwlock_wrlock(&listlock)) { /* no write lock, give up */ errno = error; return -1; } key = accessdata(); if (key == -1) { errorkey = errno; pthread_rwlock_unlock(&listlock); errno = errorkey; return -1; } if (error = pthread_rwlock_unlock(&listlock)) { errno = error; return -1; } return key; } int adddata_r(data_t data) { /* allocate a node on list to hold data */ int error; if (error = pthread_rwlock_wrlock(&listlock)) { /* no writer lock, give up */ errno = error; return -1; } if (adddata(data) == -1) { error = errno; pthread_rwlock_unlock(&listlock); errno = error; return -1; } if (error = pthread_rwlock_unlock(&listlock)) { errno = error; return -1; } return 0; } int getdata_r(int key, data_t *datap) { /* retrieve node by key */ int error; if (error = pthread_rwlock_rdlock(&listlock)) { /* no reader lock, give up */ errno = error; return -1; } if (getdata(key, datap) == -1) { error = errno; pthread_rwlock_unlock(&listlock); errno = error; return -1; } if (error = pthread_rwlock_unlock(&listlock)) { errno = error; return -1; } return 0; } int freekey_r(int key) { /* free the key */ int error; if (error = pthread_rwlock_wrlock(&listlock)) { errno = error; return -1; } if (freekey(key) == -1) { error = errno; pthread_rwlock_unlock(&listlock); errno = error; return -1; } if (error = pthread_rwlock_unlock(&listlock)) { errno = error; return -1; } return 0; }
Example 13.30.
The use of Program 13.16 requires a call to initialize_r
at least once by some thread before any threads call other functions in this library. How could this be avoided?
Answer:
The function initialize_r
can be given internal linkage by having the other functions in the library call it before accessing the lock.
Unfortunately, POSIX lists strerror
as one of the few functions that are not thread-safe. Often, this is not a problem since often the main thread is the only thread that prints error messages. If you need to use strerror
concurrently in a program, you will need to protect it with mutex locks. Neither perror
nor strerror
is async-signal safe. One way to solve both the thread-safety and async-signal-safety problems is to encapsulate the synchronization in a wrapper, as shown in Program 13.17.
The perror_r
and strerror_r
functions are both thread-safe and async-signal safe. They use a mutex to prevent concurrent access to the static buffer used by strerror
. The perror
function is also protected by the same mutex to prevent concurrent execution of strerror
and perror
. All signals are blocked before the mutex is locked. If this were not done and a signal were caught with the mutex locked, a call to one of these from inside the signal handler would deadlock.
Programs that use synchronization constructs have the potential for deadlocks that may not be detected by implementations of the POSIX base standard. For example, suppose that a thread executes pthread_mutex_lock
on a mutex that it already holds (from a previously successful pthread_mutex_lock
). The POSIX base standard states that pthread_mutex_lock
may fail and return EDEADLK
under such circumstances, but the standard does not require the function to do so. POSIX takes the position that implementations of the base standard are not required to sacrifice efficiency to protect programmers from their own bad programming. Several extensions to POSIX allow more extensive error checking and deadlock detection.
Another type of problem arises when a thread that holds a lock encounters an error. You must take care to release the lock before returning from the thread, or other threads might be blocked.
Threads with priorities can also complicate matters. A famous example occurred in the Mars Pathfinder mission. The Pathfinder executed a “flawless” Martian landing on July 4, 1997, and began gathering and transmitting large quantities of scientific data to Earth [34]. A few days after landing, the spacecraft started experiencing total system resets, each of which delayed data collection by a day. Several accounts of the underlying causes and the resolution of the problem have appeared, starting with a keynote address at the IEEE Real-Time Systems Symposium on Dec. 3, 1997, by David Wilner, Chief Technical Officer of Wind River [61].
Example 13.17. strerror_r.c
Async-signal-safe, thread-safe versions of strerror
and perror
.
#include <errno.h> #include <pthread.h> #include <signal.h> #include <stdio.h> #include <string.h> static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; int strerror_r(int errnum, char *strerrbuf, size_t buflen) { char *buf; int error1; int error2; int error3; sigset_t maskblock; sigset_t maskold; if ((sigfillset(&maskblock)== -1) || (sigprocmask(SIG_SETMASK, &maskblock, &maskold) == -1)) return errno; if (error1 = pthread_mutex_lock(&lock)) { (void)sigprocmask(SIG_SETMASK, &maskold, NULL); return error1; } buf = strerror(errnum); if (strlen(buf) >= buflen) error1 = ERANGE; else (void *)strcpy(strerrbuf, buf); error2 = pthread_mutex_unlock(&lock); error3 = sigprocmask(SIG_SETMASK, &maskold, NULL); return error1 ? error1 : (error2 ? error2 : error3); } int perror_r(const char *s) { int error1; int error2; sigset_t maskblock; sigset_t maskold; if ((sigfillset(&maskblock) == -1) || (sigprocmask(SIG_SETMASK, &maskblock, &maskold) == -1)) return errno; if (error1 = pthread_mutex_lock(&lock)) { (void)sigprocmask(SIG_SETMASK, &maskold, NULL); return error1; } perror(s); error1 = pthread_mutex_unlock(&lock); error2 = sigprocmask(SIG_SETMASK, &maskold, NULL); return error1 ? error1 : error2; }
The Mars Pathfinder flaw was found to be a priority inversion on a mutex [105]. A thread whose job was gathering meteorological data ran periodically at low priority. This thread would acquire the mutex for the data bus to publish its data. A periodic high-priority information thread also acquired the mutex, and occasionally it would block, waiting for the low-priority thread to release the mutex. Each of these threads needed the mutex only for a short time, so on the surface there could be no problem. Unfortunately, a long-running, medium-priority communication thread occasionally preempted the low-priority thread while the low-priority thread held the mutex, causing the high-priority thread to be delayed for a long time.
A second aspect of the problem was the system reaction to the error. The system expected the periodic high-priority thread to regularly use the data bus. A watchdog timer thread would notice if the data bus was not being used, assume that a serious problem had occurred, and initiate a system reboot. The high-priority thread should have been blocked only for a short time when the low-priority thread held the mutex. In this case, the high-priority thread was blocked for a long time because the low-priority thread held the mutex and the long-running, medium-priority thread had preempted it.
A third aspect was the test and debugging of the code. The Mars Pathfinder system had debugging code that could be turned on to run real-time diagnostics. The software team used an identical setup in the lab to run in debug mode (since they didn’t want to debug on Mars). After 18 hours, the laboratory version reproduced the problem, and the engineers were able to devise a patch. Glenn Reeves [93], leader of the Mars Pathfinder software team, was quoted as saying “We strongly believe in the ’test what you fly and fly what you test’ philosophy.” The same ideas apply here on Earth too. At a minimum, you should always think about instrumenting code with test and debugging functions that can be turned on or off by conditional compilation. When possible, allow debugging functions to be turned on dynamically at runtime.
A final aspect of this story is timing. In some ways, the Mars Pathfinder was a victim of its own success. The software team did extensive testing within the parameters of the mission. They actually saw the system reset problem once or twice during testing, but did not track it down. The reset problem was exacerbated by high data rates that caused the medium-priority communication thread to run longer than expected. Prelaunch testing was limited to “best case” high data rates. In the words of Glenn Reeves, “We did not expect nor test the ’better than we could have ever imagined’ case.” Threaded programs should never rely on quirks of timing to work—they must work under all possible timings.
Reimplement the barrier of Program 13.13 so that it supports multiple barriers. One possible approach is to use an array or a linked list of barriers. Explore different designs with respect to synchronization. Is it better to use a single bmutex
lock and bcond
condition variable to synchronize all the barriers, or should each barrier get its own synchronization? Why?
Most operating systems books spend some time on synchronization and use of standard synchronization mechanisms such as mutex locks, condition variables and read-write locks. The review article “Concepts and notations for concurrent programming,” by Andrews and Schneider [3] gives an excellent overview of much of the classical work on synchronization. “Interrupts as threads” by Kleiman and Eykholt [63] discusses some interesting aspects of the interaction of threads and interrupts in the kernel. An extensive review of monitors can be found in “Monitor classification,” by Buhr et al. [17]. The signal and wait operations of monitors are higher-level implementations of the mutex-conditional variable combination. The Solaris Multithreaded Programming Guide [109], while dealing primarily with Solaris threads, contains some interesting examples of synchronization. Finally, the article “Schedule-conscious synchronization” by Kontothanassis et al. [65] discusses implementation of mutex locks, read-write locks and barriers in a multiprocessor environment.