Chapter 13. Thread Synchronization

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.

POSIX Synchronization Functions

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

pthread_mutex_destroy

pthread_mutex_init

pthread_mutex_lock

pthread_mutex_trylock

pthread_mutex_unlock

condition variables

pthread_cond_broadcast

pthread_cond_destroy

pthread_cond_init

pthread_cond_signal

pthread_cond_timedwait

pthread_cond_wait

read-write locks

pthread_rwlock_destroy

pthread_rwlock_init

pthread_rwlock_rdlock

pthread_rwlock_timedrdlock

pthread_rwlock_timedwrlock

pthread_rwlock_tryrdlock

pthread_rwlock_trywrlock

pthread_rwlock_wrlock

Mutex 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.

Creating and initializing a mutex

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

EAGAIN

system lacks nonmemory resources needed to initialize *mutex

ENOMEM

system lacks memory resources needed to initialize *mutex

EPERM

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.

Example 13.3. 

What happens if a thread tries to initialize a mutex that has already been initialized?

Answer:

POSIX explicitly states that the behavior is not defined, so avoid this situation in your programs.

Destroying a mutex

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.

Example 13.4. 

The following code segment destroys a mutex.

pthread_mutex_t mylock;

if (error = pthread_mutex_destroy(&mylock))
   fprintf(stderr, "Failed to destroy mylock:%s
", strerror(error));

Example 13.5. 

What happens if a thread references a mutex after it has been destroyed? What happens if one thread calls pthread_mutex_destroy and another thread has the mutex locked?

Answer:

POSIX explicitly states that the behavior in both situations is not defined.

Locking and unlocking a mutex

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

EINVAL

mutex has protocol attribute PTHREAD_PRIO_PROTECT and caller’s priority is higher than mutex’s current priority ceiling (pthread_mutex_lock or pthread_mutex_trylock)

EBUSY

another thread holds the lock (pthread_mutex_trylock)

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);
}

Protecting unsafe library functions

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);
}

Synchronizing flags and global values

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;
}

Making data structures thread-safe

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.

At-Most-Once and At-Least-Once-Execution

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.

  1. Thread A calls testandsetonce.

  2. The testandsetonce returns in thread A.

  3. Thread A loses the CPU.

  4. Thread B calls testandsetonce.

  5. The executeonce returns to thread B without printing or initializing var.

  6. Thread B assumes that var has been initialized, and it increments the variable.

  7. 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.

Condition Variables

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.

  1. Lock a mutex.

  2. Test the condition x==y.

  3. If true, unlock the mutex and exit the loop.

  4. 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.

Creating and destroying condition variables

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

EAGAIN

system lacked nonmemory resources needed to initialize *cond

ENOMEM

system lacked memory resources needed to initialize *cond

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.

Example 13.18. 

The following code segment destroys the condition variable tcond.

pthread_cond_t tcond;

if (error = pthread_cond_destroy(&tcond))
   fprintf(stderr, "Failed to destroy tcond:%s
", strerror(error));

Example 13.19. 

What happens if a thread references a condition variable that has been destroyed?

Answer:

POSIX explicitly states that the results are not defined. The standard also does not define what happens when a thread attempts to destroy a condition variable on which other threads are blocked.

Waiting and signaling on condition variables

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.

  1. Acquire the mutex before testing the predicate.

  2. 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.

  3. Acquire the mutex before changing any of the variables appearing in the predicate.

  4. Hold the mutex only for a short period of time—usually while testing the predicate or modifying shared variables.

  5. 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;
}

Signal Handling and Threads

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 (pthread_kill)

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.

Directing a signal to a particular thread

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

EINVAL

sig is an invalid or unsupported signal number

ESRCH

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.

Masking signals for threads

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).

Dedicating threads for signal handling

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, &param)) {
      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, &param)) ||
        (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, &param))
         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.

Readers and Writers

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

EAGAIN

system lacked nonmemory resources needed to initialize *rwlock

ENOMEM

system lacked memory resources needed to initialize *rwlock

EPERM

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.

A strerror_r Implementation

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.

Deadlocks and Other Pesky Problems

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.

Exercise: Multiple Barriers

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?

Additional Reading

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset