8.4. Data synchronization between Pthreads

Synchronization is a programming method that allows multiple Pthreads to coordinate their data accesses, therefore avoiding the situation where one Pthread can change a piece of data at the same time another one is reading or writing the same piece of data. This situation is commonly called a race condition.

Consider, for example, a single counter, X, that is incremented by two Pthreads, A and B. If X is originally 1, then by the time Pthreads A and B increment the counter, X should be 3. Both Pthreads are independent entities and have no synchronization between them. If both Pthreads are executed concurrently on two CPUs, or if the scheduling makes the Pthreads alternatively execute on each instruction, the following steps may occur:

  • Pthread A executes the first instruction and puts X, which is 1, into the Pthread A register. Then, Pthread B executes and puts X, which is 1, into the Pthread B register.

  • Next, Pthread A executes the second instruction and increments the content of its register to 2. Then, Pthread B increments its register to 2. Nothing is moved to memory X, so memory X stays the same.

  • Last, Pthread A moves the content of its register, which is now 2, into memory X. Then, Pthread B moves the content of its register, which is also 2, into memory X, overwriting Pthread A’s value.

Note that, in most cases, Pthread A and Pthread B will execute the three instructions one after the other, and the result would be 3, as expected. as expected. Race conditions are usually difficult to discover because they occur intermittently.

To avoid this race condition, each Pthread should lock the data before accessing the counter and updating memory X. For example, if Pthread A takes a lock and updates the counter, it leaves memory X with a value of 2. Once Pthread A releases the lock, Pthread B takes the lock and updates the counter, taking 2 as its initial value for X and incrementing it to 3, the expected result.

To write a program of any complexity using Pthreads, you will need to share data between Pthreads, or cause various actions to be performed in some coherent order across multiple Pthreads. To do this, you need to synchronize the activity of the Pthreads. Data synchronization among Pthreads can be done using any of the three types of locking primitive: mutexs, condition variables, and read-write locks, as explained in the following sections:

  • Section 8.4.1, “Synchronizing Pthreads with mutexes” on page 297

  • Section 8.4.2, “Synchronizing Pthreads with condition variables” on page 303

  • Section 8.4.3, “Synchronizing Pthreads with read-write locks” on page 310

Although these sections provide enough information to understand the concept of these locking primitives, data synchronization in the multi-threaded programming environment can be a challenging task. For further information about it, please refer to the following publications:

  • Programming with POSIX Threads, by Lewine

  • POSIX Programmer’s Guide: Writing Portable UNIX Programs, by Butenhof

8.4.1. Synchronizing Pthreads with mutexes

One of the basic problems when running several Pthreads that use the same memory space is making sure that they do not interfere with each other. By this, we refer to the problem of using a data structure from two different Pthreads.

The mutual exclusion lock (mutex) is the simplest synchronization primitive provided by the Pthread library, and many of the other synchronization primitives are built upon it.

It is based on the concept of a resource that only one person can use, in a period of time, for example, a chair or a pencil. If one person sits in a chair, no one can sit on it until the first person stands up. This kind of primitive is quite useful for creating critical sections. A critical section is a portion of code that must run atomically because they normally are handling resources, such as file descriptors, I/O devices, or shared data. A critical section is a portion of code delimited by the instructions that lock and unlock a mutex variable. Ensuring that all Pthreads acting on the same resource or shared data obey this rule is a very good practice to avoid trouble when programming with Pthreads. A mutex is a lock that guarantees three things:

AtomicityLocking a mutex is an atomic operation, meaning that the operating system (or Pthreads library) assures you that if you locked a mutex, no other Pthread succeeded in locking this mutex at the same time.
SingularityIf a Pthread managed to lock a mutex, it is assured that no other Pthread will be able to lock the Pthread until the original Pthread releases the lock.
Non-busy waitIf a Pthread attempts to lock a Pthread that was locked by a second Pthread, the first Pthread will be suspended (and will not consume any CPU resources) until the lock is freed by the second Pthread. At this time, the first Pthread will wake up and continue execution, having the mutex locked by it.

Very often, the action performed by a Pthread owning a mutex is the updating of global variables. This is a safe way to ensure that when several Pthreads update the same variable, the final value is the same as what it would be if only one Pthread performed the update. The variables being updated belong to a critical section. A typical sequence of using mutex is as follows:

  1. Create and initialize the mutex variable.

  2. Several Pthreads attempt to lock the mutex.

  3. Only one succeeds and that Pthread owns the mutex.

  4. The owner Pthread performs some set of actions.

  5. The owner unlocks the mutex.

  6. Another Pthread acquires the mutex and repeats the process.

  7. Finally, the mutex is destroyed.

Creating and initializing a mutex

In order to create a mutex, we first need to declare a variable of type pthread_mutex_t, and then initialize it. The simplest way is by assigning it the PTHREAD_MUTEX_INITIALIZER constant. For example, statically, it is declared as follows:

pthread_mutex_t a_mutex = PTHREAD_MUTEX_INITIALIZER;

A mutex variable can also be dynamically initialized with the pthread_mutex_init() routine. This method permits the setting of mutex object attributes.

The pthread_mutexattr_init() and pthread_mutexattr_destroy() routines are used to create and destroy mutex attribute objects respectively.

Locking and unlocking a mutex

In order to lock a mutex, we may use the pthread_mutex_lock() function. This function attempts to lock the mutex, or block the Pthread if the mutex is already locked by another Pthread. In this case, when the mutex is unlocked by the first Pthread, the function will return with the mutex locked by the other Pthreads. Here is how to lock a mutex (assuming it was initialized earlier):

int rc = pthread_mutex_lock(&a_mutex);
if (rc) {
    /* an error has occurred */
    fprintf(stderr, "pthread_mutex_lock() failed with rc = %d.
", rc);
    pthread_exit(NULL);
}
/* mutex is now locked - do your stuff. */
...

After the Pthread did what it had to (change variables or data structures, handle file, or whatever it intended to do), it should free the mutex, using the pthread_mutex_unlock() function, as follows:

rc = pthread_mutex_unlock(&a_mutex);
if (rc) {
    fprintf(stderr, "pthread_mutex_unlock() failed with rc = %d.
", rc);
    pthread_exit(NULL);
}

Destroying a mutex

After we finished using a mutex, we should destroy it. Finished using it means no Pthread needs it at all. If only one Pthread is finished with the mutex, it should leave it alive for the other Pthreads that might need to use it. Once all Pthreads finished using it, the last one can destroy it using the pthread_mutex_destroy() function:

rc = pthread_mutex_destroy(&a_mutex);

The pthread_mutex_destroy function destroys the mutex object referenced by mutex; the mutex object becomes, in effect, uninitialized. An implementation may cause pthread_mutex_destroy() to set the object referenced by mutex to an invalid value. A destroyed mutex object can be re-initialized using pthread_mutex_init(); the results of otherwise referencing the object after it has been destroyed are undefined.

It is safe to destroy an initialized mutex that is unlocked. Attempting to destroy a locked mutex results in undefined behavior.

An example of using mutexs

The example program shown in Example 8-7 on page 300 illustrates the use of mutex variables in a multi-threaded program that increments a counter variable declared globally. Each Pthread works on the same data. The initial thread waits for all the other Pthreads to complete their executions, and then it prints the resulting sum.

This program demonstrates how to use mutexes to synchronize the operation of Pthreads. In this program, the main Pthread creates two Pthreads. Each Pthread tries to access a global variable counter, increment its value, and print it. A mutex is used to allow only a single Pthread to access the counter at any point of time.

Example 8-7. pthread_mutex.c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define EXIT_CODE -1

pthread_t tid1, tid2;    /* thread IDs. */
pthread_attr_t t_attr;   /* thread attribute structure. */
pthread_mutex_t mut1;    /* mutex. */
pthread_mutexattr_t attr;/* mutex attribute structure. */
int counter = 0;         /* global data that will be accessed by two threads.*/

/* first thead executes this function. */
void* mythread1(void* arg)
{
    int rc;
    while (counter < 10) {
        /* block until the mutex lock is obtained, lock when you get it. */
        if ((rc = pthread_mutex_lock(&mut1)) != 0) {
            fprintf(stderr
                , "pthread_mutex_lock() failed with rc = %d.
", rc);
            exit(EXIT_CODE);
        }
        if (counter < 10) {
            counter++;
        }
        fprintf(stderr,"Thread 1 = %02d
 ", counter);
        /* release or unlock the mutex */
        if ((rc = pthread_mutex_unlock(&mut1)) != 0) {
            fprintf(stderr
                , "pthread_mutex_unlock() failed with rc = %d.
", rc);
            exit(EXIT_CODE);
        }
        sleep(1);
    }
}

/* second thread executes this function */
void* mythread2(void* arg)
{
    int rc;
    while (counter < 10) {
        /* block untill the mutex lock is obtained, lock when you get it. */
        if ((rc = pthread_mutex_lock(&mut1)) != 0) {
            fprintf(stderr
                , "pthread_mutex_lock() failed with rc = %d.
", rc);
            exit(EXIT_CODE);
        }
        if (counter < 10) {
            counter++;
        }
        printf("Thread 2 = %02d
", counter);
        /* release or unlock the mutex */
        if ((rc = pthread_mutex_unlock(&mut1)) !=0) {
            fprintf(stderr
                , "pthread_mutex_unlock() failed with rc = %d.
", rc);
            exit(EXIT_CODE);
        }
        sleep(1);
    }
}

/* main thread of execution. */
int main(int argc, char* argv[])
{
    int rc, status;
    /* create the mutex attribute structure. */
    if ((rc = pthread_mutexattr_init(&attr)) != 0) {
        fprintf(stderr, "pthread_mutexattr_init() failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    /* initialize the mutex to be used. */
    if ((rc = pthread_mutex_init(&mut1, &attr)) != 0) {
        fprintf(stderr, "pthread_mutex_init() failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    /* initialize the attribute structure for the threads to be created. */
    if ((rc = pthread_attr_init(&t_attr)) !=0) {
        fprintf(stderr, "pthread_attr_init() failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    /* create the first thread. */
    if ((rc = pthread_create(&tid1, &t_attr, mythread1, 0)) != 0) {
        fprintf(stderr, "pthread_create(#1) failed with rc = %d.
", rc);
        exit(EXIT_CODE);

    }
    /* create the first thread. */
    if ((rc = pthread_create(&tid2, &t_attr, mythread2, 0)) != 0) {
        fprintf(stderr, "pthread_create(#2) failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    pthread_attr_destroy(&t_attr);
    pthread_join(tid1, (void **)&status);
    pthread_join(tid2, (void **)&status);

    printf("
Sum is = %d
", counter);
    pthread_mutex_destroy(&mut1);
    pthread_exit(NULL);
}

The output of the above program is:

Thread 1 = 01
Thread 2 = 02
Thread 1 = 03
Thread 2 = 04
Thread 1 = 05
Thread 2 = 06
Thread 1 = 07
Thread 2 = 08
Thread 1 = 09
Thread 2 = 10

Sum is = 10

Please note that the program examines the value of the counter again after it examines the value in the while condition (highlighted in Example 8-7 on page 300). If it does not, the program results in a potential race condition, so that the value of the counter could be 11 instead of 10.

Starvation and deadlock situations

We should remember that pthread_mutex_lock() might block for a undetermined duration if the mutex is already locked. If it remains locked forever, it is said that our Pthread is starved, that is, it tried to acquire a resource, but never got it. It is up to the programmer to ensure that such starvation will not occur. The Pthread library does not help us with that.

The Pthread library might, however, figure out a deadlock. A deadlock is a situation in which a set of Pthreads are all waiting for resources taken by other Pthreads, all in the same set. Naturally, if all Pthreads are blocked waiting for a mutex, none of them will ever come back to life again. The Pthread library keeps track of such situations, and thus would fail the last Pthread trying to call pthread_mutex_lock() with an error of type EDEADLK. The programmer should check for such a value, and take steps to solve the deadlock somehow.

The following code fragment will result in a deadlock:

pthread_mutex_t mutex_A;
pthread_mutex_t mutex_B;

int counter_A = 0;
int counter_B = 0;
/* func_1() will be executed by the Pthread #1. */
void func_1()
{
   pthread_mutex_lock(&mutex_A);
   counter_A++;

   pthread_mutex_lock(&mutex_B);
   counter_B++;
   pthread_mutex_unlock(&mutex_B);

   pthread_mutex_unlock(&mutex_A);
}

/* func_2() will be executed by the Pthread #2. */
void func_2()
{
   pthread_mutex_lock(&mutex_B);
   counter_B++;

   pthread_mutex_lock(&mutex_A);
   counter_A++;
   pthread_mutex_unlock(&mutex_A);

   pthread_mutex_unlock(&mutex_B);
}

In the above example, while mutex_A is already locked by Pthread #1 executing func_1(), multex_B can be also locked by Pthread #2 executing func_B(). Both Pthreads wait for a mutex until the other Pthread releases it. To avoid this situation, both Pthreads must try to acquire to lock in the same order.

8.4.2. Synchronizing Pthreads with condition variables

Condition variables provide yet another way for Pthreads to synchronize. While mutexes implement synchronization by controlling Pthread access to data, condition variables allow Pthreads to synchronize based upon the actual value of data. Without condition variables, the programmer would need to have Pthreads continually polling (possibly in a critical section) to check if the condition is met. This can be very resource consuming since the Pthread would be continuously busy in this activity. A condition variable is a way to achieve the same goal without polling.

What is a condition variable

A condition variable is a mechanism that allows Pthreads to wait (without wasting CPU cycles) for some event to occur. Several Pthreads may wait on a condition variable, until some other Pthread signals this condition variable (thus sending a notification). At this time, one of the Pthreads waiting on this condition variable wakes up, and can act on the event. It is possible to also wake up all Pthreads waiting on this condition variable by using a broadcast method on this variable.

Note that a condition variable itself does not provide any kind of locking. Thus, a mutex is used along with the condition variable to provide the necessary locking when accessing this condition variable.

Creating and initializing a condition variable

Condition variables must be declared with pthread_cond_t, and must be initialized before they can be used. There are two ways to initialize a condition variable.

  • Statically, when it is declared. For example,

    pthread_cond_t a_cond_var = PTHREAD_COND_INITIALIZER;
    
  • To initialize the condition variable at run time, use the pthread_cond_init() routine. This method permits setting condition variable object attributes. The pthread_condattr_init() and pthread_condattr_destroy() routines are used to create and destroy condition variable attribute objects.

Signalling a condition variable

In order to signal a condition variable, one should either use the pthread_cond_signal() function (to wake up only one Pthread waiting on this variable), or the pthread_cond_broadcast() function (to wake up all Pthreads waiting on this variable). Here is an example of using a signal, assuming a_cond_var is a properly initialized condition variable:

int rc = pthread_cond_signal (&a_cond_var);

Or by using the broadcast function:

int rc = pthread_cond_broadcast(&a_cond_var);

When either function returns, the return code rc is set to 0 on success, and to a non-zero value on failure. In case of failure, the return code denotes the error that occurred; EINVAL denotes that the given parameter is not a condition variable and ENOMEM denotes that the system has run out of memory.

The success of a signaling operation does not mean any Pthread was awakened. It might be that no Pthread was waiting on the condition variable, and thus the signaling does nothing (that is, the signal is lost).

Note

The term signal, as used in this section, is a different concept from the one used in the UNIX signal mechanism.


Waiting on a condition variable

If one Pthread signals the condition variable, other Pthreads would probably want to wait for this signal. They may do so using one of two functions, pthread_cond_wait() or pthread_cond_timedwait(). Each of these functions takes a condition variable and a mutex (which should be locked before calling the wait function), unlocks the mutex, and waits until the condition variable is signaled, suspending the Pthread’s execution. If this signaling causes the Pthread to awake (see the discussion about pthread_cond_signal() in “Signalling a condition variable” on page 304), the mutex is automatically locked again by the wait function, and the wait function returns.

The only difference between these two functions is that pthread_cond_timedwait() allows the programmer to specify a timeout for the waiting, after which the function always returns with a proper error value (ETIMEDOUT) to notify that condition variable was not signaled before the timeout passed. The pthread_cond_wait() would wait indefinitely if it was never signaled.

The code fragment shown in Example 8-8 depicts how to use pthread_cond_wait(). We make the assumption that a_cond_var is a properly initialized condition variable, and that request_mutex is a properly initialized mutex.

Example 8-8. An example of using pthread_cond_wait()
/* first, lock the mutex. */
int rc;
if ((rc = pthread_mutex_lock(&a_mutex)) !=0) {
    /* an error has occurred. */
    fprintf(stderr, "pthread_mutex_lock() failed with rc = %d.
", rc);
    pthread_exit(NULL);
}
/* mutex is now locked - wait on the condition variable.             */
/* During the execution of pthread_cond_wait, the mutex is unlocked. */
if ((rc = pthread_cond_wait(&a_cond_var, &a_mutex)) == 0) {
    /*
     * we were awakened due to the condition variable, which had been signaled.
     * The mutex is now locked again by pthread_cond_wait().
     * do your stuff....
     */
    ...
}
/* finally, unlock the mutex. */
pthread_mutex_unlock(&a_mutex);

The code fragment shown in Example 8-9 on page 306 shows how to use pthread_cond_timedwait().

Example 8-9. An example using pthread_cond_timedwait()
#include <sys/time.h>              /* struct timeval definition.          */
#include <unistd.h>                /* declaration of gettimeofday().      */

struct timeval  now;               /* time when we started waiting.       */
struct timespec timeout;           /* timeout value for the wait function.*/
int             done;              /* are we done waiting?                */

/* first, lock the mutex. */
int rc = pthread_mutex_lock(&a_mutex);
if (rc) {
    /* an error has occurred. */
    fprintf(stderr, "pthread_mutex_lock() failed with rc = %d.
", rc);
    pthread_exit(NULL);
}
/* mutex is now locked. */

/* get current time. */
gettimeofday(&now);
/* prepare timeout value. */
/* t_sec member is represented in seconds, while tv_usec in microseconds. */
timeout.tv_sec = now.tv_sec + 5;
timeout.tv_nsec = now.tv_usec * 1000;

/* wait on the condition variable. */
/* we use a loop, since a UNIX signal might stop the wait before the timeout. */
done = 0;
while (!done) {
    /* remember that pthread_cond_timedwait() unlocks the mutex on entrance. */
    rc = pthread_cond_timedwait(&a_cond_var, &a_mutex, &timeout);
    switch(rc) {
    case 0:
        /*
         * we were awakened due to the condition variable
         * , which had been signaled.
         * The mutex is now locked again by pthread_cond_wait().
         * do your stuff....
         */
        ...
        done = 0;
        break;
    case ETIMEDOUT: /* our time is up. */
        done = 0;
        break;
    default:        /* some error occurred (e.g. we got a UNIX signal). */
        break;      /* break this switch, but re-do the while loop.     */
    }
}
/* finally, unlock the mutex. */
pthread_mutex_unlock(&a_mutex);

It might be that a condition variable that has two or more Pthreads waiting on it is signaled many times, and yet one of the Pthreads waiting on it never awakens. This is because we are not guaranteed which of the waiting Pthreads is awakened when the variable is signaled. It might be that the awakened Pthread quickly comes back to waiting on the condition variables, and gets awakened again when the variable is signaled again, and so on. The situation for the un-awakened Pthread is called starvation. It is up to the programmer to make sure that this situation does not occur if it implies bad behavior.

When the mutex is being broadcast using pthread_cond_broadcast(), this does not mean all Pthreads are running together. Each of them tries to lock the mutex again before returning from their wait function, and thus they will start running one by one, each one locking the mutex, doing their work, and freeing the mutex before the next Pthread gets its chance to run.

Destroying a condition variable

After we are done using a condition variable, we should destroy it to free any system resources it might be using. This can be done using pthread_cond_destroy(). In order for this to work, there should be no Pthreads waiting on this condition variable. Here is how to use this function, again assuming a_cond_var is a pre-initialized condition variable:

int rc;

if ((rc = pthread_cond_destroy(&a_cond_var)) == EBUSY) {
    /* some Pthread is still waiting on this condition variable. */
    /* handle this case here... */
}

What if some Pthread is still waiting on this variable? Depending on the case, it might imply some flaw in the usage of this variable, or just lack of proper Pthread cleanup code.

Using a condition variable

The example program shown in Example 8-10 on page 308 demonstrates the use of several Pthread condition variable routines. The main routine creates three Pthreads. Two Pthreads that execute the add_counter() function increment a variable, named count, whenever the associated mutex is held. The third Pthread that executes the monitor_counter() function waits until the count variable reaches a specified value.

Example 8-10. pthread_cond_variable.c
#include <pthread.h>
#include <stdio.h>

#define NUM_OF_THREADS 3
#define TOTAL_COUNT 5
#define MAX_COUNT 5
#define EXIT_CODE -1

int     count = 0;
int     thread_ids[3] = {0, 1, 2};
pthread_mutex_t a_mutex;
pthread_cond_t a_cond_var;

void *add_counter(void *arg)
{
    unsigned int i, j;
    double result = 0.0;
    int *my_id = arg;

    for (i = 0; i < TOTAL_COUNT; i++) {
        pthread_mutex_lock(&a_mutex);
        count++;

        /*
         * Check the value of count and signal waiting thread when condition is
         * reached. Note that this occurs while mutex is locked.
         */
        if (count == MAX_COUNT) {
            pthread_cond_signal(&a_cond_var);
            printf(
                "add_counter(): thread %d, count = %d, threshold reached.
"
                , *my_id, count);
        }
        printf("add_counter(): thread %d, count = %d, unlocking mutex.
"
            , *my_id, count);
        pthread_mutex_unlock(&a_mutex);

        /* Do some work so threads can alternate on mutex lock. */
        for (j = 0; j < 1000; j++)
            result += j;
        }
        pthread_exit(NULL);
}

void *monitor_counter(void *arg)
{
    int *my_id = arg;
    printf("Starting monitor_counter(): thread %d
", *my_id);

    /*
     * Lock mutex and wait for signal. Note that the pthread_cond_wait()
     * routine will automatically and atomically unlock mutex while it waits.
     * Also, note that if MAX_COUNT is reached before this routine is run by
     * the waiting thread, the loop will be skipped to prevent
     * pthread_cond_wait() from never returning.
     */
    pthread_mutex_lock(&a_mutex);
    while (count < MAX_COUNT) {
        pthread_cond_wait(&a_cond_var, &a_mutex);
        printf("monitor_counter(): thread %d Condition signal received.
"
            , *my_id);
    }
    pthread_mutex_unlock(&a_mutex);
    pthread_exit(NULL);
}

int main (int argc, char *argv[])
{
    int i, rc;
    pthread_t threads[NUM_OF_THREADS];
    pthread_attr_t attr;

    /* Initialize mutex and condition variable objects. */
    pthread_mutex_init(&a_mutex, NULL);
    pthread_cond_init(&a_cond_var, NULL);

    /*
     * For portability, explicitly create threads in a joinable state
     * so that they can be joined later.
     */
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
    if ((rc = pthread_create(&threads [0], &attr, add_counter
                , (void *)&thread_ids[0])) != 0) {
        fprintf(stderr, "pthread_create(#1) is failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    if ((rc = pthread_create(&threads[1], &attr, add_counter
                , (void *)&thread_ids[1])) != 0) {
        fprintf(stderr, "pthread_create(#2) is failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    if ((rc = pthread_create (&threads [2], &attr, monitor_counter
                , (void *) &thread_ids [2])) != 0) {
        fprintf(stderr, "pthread_create (#3) is failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }

    /* Wait for all threads to complete */
    for (i = 0; i < NUM_OF_THREADS; i++) {
        if ((rc = pthread_join (threads [i], NULL)) != 0) {
            fprintf(stderr, "pthread_join() is failed with rc = %d.
", rc);
            exit(EXIT_CODE);
        }
    }
    printf ("Main(): Waited on %d threads. Done...
", NUM_OF_THREADS);

    /* Clean up and exit */
    pthread_attr_destroy(&attr);
    pthread_mutex_destroy (&a_mutex);
    pthread_cond_destroy (&a_cond_var);
    pthread_exit (NULL);
}

The output of this program is:

add_counter(): thread 0, count = 1, unlocking mutex.
add_counter(): thread 0, count = 2, unlocking mutex.
Starting monitor_counter(): thread 2
add_counter(): thread 0, count = 3, unlocking mutex.
add_counter(): thread 1, count = 4, unlocking mutex.
add_counter(): thread 0, count = 5, threshold reached.
add_counter(): thread 0, count = 5, unlocking mutex.
add_counter(): thread 1, count = 6, unlocking mutex.
add_counter(): thread 0, count = 7, unlocking mutex.
add_counter(): thread 1, count = 8, unlocking mutex.
monitor_counter(): thread 2 Condition signal received.
add_counter(): thread 1, count = 9, unlocking mutex.
add_counter(): thread 1, count = 10, unlocking mutex.
Main(): Waited on 3 threads. Done...

8.4.3. Synchronizing Pthreads with read-write locks

Another type of lock primitive, read-write locks allow a Pthread to exclusively lock some shared data while updating that data, or allow any number of Pthreads to have simultaneous read-only access to the data.

Unlike a mutex, a read-write lock distinguishes between reading data and writing data. A mutex excludes all other Pthreads. A read-write lock allows other Pthreads access to the data, providing no Pthread is modifying the data. Thus, a read-write lock is less primitive than either a mutex-condition variable pair or a semaphore.

Application developers should consider using a read-write lock rather than a mutex to protect data that is frequently referenced but seldom modified. Most Pthreads (readers) will be able to read the data without waiting and will only have to block when some other Pthread (a writer) is in the process of modifying the data. Conversely, a Pthread that wants to change the data is forced to wait until there are no readers. This type of lock is often used to facilitate parallel access to data on multiprocessor platforms or to avoid context switches on single processor platforms where multiple Pthreads access the same data.

If a read-write lock becomes unlocked and there are multiple Pthreads waiting to acquire the write lock, the implementation’s scheduling policy determines which Pthread shall acquire the read-write lock for writing. If there are multiple Pthreads blocked on a read-write lock for both read locks and write locks, it is unspecified whether the readers or a writer acquires the lock first. However, for performance reasons, implementations often favor writers over readers to avoid potential writer starvation.

A read-write lock object is an implementation-dependent opaque object of type pthread_rwlock_t, as defined in pthread.h. There are two different sorts of locks associated with a read-write lock: a read lock and a write lock.

The pthread_rwlockattr_init() function initializes a read-write lock attributes object with the default value for all the attributes defined in the implementation. After a read-write lock attributes object has been used to initialize one or more read-write locks, changes to the read-write lock attributes object, including destruction, do not affect previously initialized read-write locks.

The read-write lock attribute can be any of the following:

PTHREAD_PROCESS_SHARED

Any Pthread of any process that has access to the memory where the read-write lock resides can manipulate the read-write lock.

PTHREAD_PROCESS_PRIVATE

Only Pthreads created within the same process as the Pthread that initialized the read-write lock can manipulate the read-write lock. This is the default value.

The pthread_rwlockattr_setpshared() function is used to set the process-shared attribute of an initialized read-write lock attributes object while the function pthread_rwlockattr_getpshared() obtains the current value of the process-shared attribute. A read-write lock attributes object is destroyed using the pthread_rwlockattr_destroy() function.

A Pthread creates a read-write lock using the pthread_rwlock_init() function. The attributes of the read-write lock can be specified by the application developer; otherwise, the default implementation-dependent read-write lock attributes are used if the pointer to the read-write lock attributes object is NULL. In cases where the default attributes are appropriate, the PTHREAD_RWLOCK_INITIALIZER macro can be used to initialize statically allocated read-write locks.

A Pthread that wants to apply a read lock to the read-write lock can use either pthread_rwlock_rdlock() or pthread_rwlock_tryrdlock(). If pthread_rwlock_rdlock() is used, the Pthread acquires a read lock if a writer does not hold the write lock and there are no writers blocked on the write lock. If a read lock is not acquired, the calling Pthread blocks until it can acquire a lock. However, if pthread_rwlock_tryrdlock() is used, the function returns immediately with the error EBUSY if any Pthread holds a write lock or there are blocked writers waiting for the write lock.

A Pthread that wants to apply a write lock to the read-write lock can use either of two functions: pthread_rwlock_wrlock() or pthread_rwlock_trywrlock(). If pthread_rwlock_wrlock() is used, the Pthread acquires the write lock if no other reader or writer Pthreads holds the read-write lock. If the write lock is not acquired, the Pthread blocks until it can acquire the write lock. However, if pthread_rwlock_trywrlock() is used, the function returns immediately with the error EBUSY if any Pthread is holding either a read or a write lock.

The pthread_rwlock_unlock() function is used to unlock a read-write lock object held by the calling Pthread.

Supported read-write lock sub-routines are listed in Table D-5 on page 480.

For further information about the read-write lock programming on AIX, please refer to the “Threads Programming Guidelines” section in AIX 5L Version 5.2 General Programming Concepts: Writing and Debugging Programs.

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

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