13.2 Mutex Locks

Team-FLY

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 .

13.2.1 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\n", 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 .

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

13.2.2 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\n", strerror(error)); 
Exercise 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.

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

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

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

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

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

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

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

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

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

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

Program 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\n", 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\n", 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\n", 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\n", i, strerror(error));             return 1;         }     if (showresults())         return 1;     return 0; } 
Program 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\n", strerror(error));       return -1;    }    if (gerror) {          /* an error occurred in compute thread computation */       fprintf(stderr, "Failed to compute sum: %s\n", strerror(gerror));        return -1;    }    if (count == 0)       printf("No values were summed.\n");    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\n", sum, count);       printf("The average is %f and error is %f or %f%%\n", average, err, perr);    }    return 0; } 

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

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

Team-FLY


Unix Systems Programming
UNIX Systems Programming: Communication, Concurrency and Threads
ISBN: 0130424110
EAN: 2147483647
Year: 2003
Pages: 274

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net