Document revision date: 30 March 2001
[Compaq] [Go to the documentation home page] [How to order documentation] [Help on this site] [How to contact us]
[OpenVMS documentation]

Guide to the POSIX Threads Library


Previous Contents Index

2.4 Synchronization Objects

In a multithreaded program, you must use synchronization objects whenever there is a possibility of conflict in accessing shared data. The following sections discuss three kinds of synchronization objects: mutexes, condition variables, and read-write locks.

2.4.1 Mutexes

A mutex (or mutual exclusion) object is used by multiple threads to ensure the integrity of a shared resource that they access, most commonly shared data, by allowing only one thread to access it at a time.

A mutex has two states, locked and unlocked. A locked mutex has an owner---the thread that locked the mutex. It is illegal to unlock a mutex not owned by the calling thread.

For each piece of shared data, all threads accessing that data must use the same mutex: each thread locks the mutex before it accesses the shared data and unlocks the mutex when it is finished accessing that data. If the mutex is locked by another thread, the thread requesting the lock either waits for the mutex to be unlocked or returns, depending on the lock routine called (see Figure 2-4).

Figure 2-4 Only One Thread Can Lock a Mutex


Each mutex must be initialized before use. The Threads Library supports static initialization of static or extern mutexes at compile time, using the PTHREAD_MUTEX_INITIALIZER macro provided in the pthread.h header file, as well as dynamic initialization at run time by calling pthread_mutex_init() . This routine allows you to specify an attributes object, which allows you to specify the mutex type. The types of mutexes are described in the following sections.

2.4.1.1 Normal Mutex

A normal mutex is the most efficient type of mutex, but also the least forgiving.

A normal mutex usually does not record or check thread ownership---that is, a deadlock will result if the owner attempts to "relock" the mutex. The system usually will not report an erroneous attempt to unlock a mutex not owned by the calling thread; this means that some potentially severe application errors may not be detected. Normal mutexes also provide less debugging information, because the owner cannot be identified.

2.4.1.2 Default Mutex

This is the name reserved by the Single UNIX Specification, Version 2, for a vendor's default mutex type. For the pthread interface, the "normal" mutex type is the "default" mutex type. Be aware that other implementations could implement "default" errorcheck, recursive, or even a nonportable mutex type.

2.4.1.3 Recursive Mutex

A recursive mutex can be locked more than once by a given thread without causing a deadlock. The thread must call the pthread_mutex_unlock() routine the same number of times that it called the pthread_mutex_lock() routine before another thread can lock the mutex.

When a thread first successfully locks a recursive mutex, it owns that mutex and the lock count is set to 1. Any other thread attempting to lock the mutex blocks until the mutex becomes unlocked. If the owner of the mutex attempts to lock the mutex again, the lock count is incremented, and the thread continues running.

When an owner unlocks a recursive mutex, the lock count is decremented. The mutex remains locked and owned until the count reaches zero. The Threads Library will always detect and report an attempt by any thread other than the owner to unlock the mutex.

A recursive mutex is useful when a routine requires exclusive access to a piece of data, and cannot tell whether its caller already owns the mutex. This is common when converting old code to be thread-safe. However, the code must ensure that the shared data is in a consistent state before calling another routine which requires access to it under the lock.

This type of mutex is called "recursive" because it allows you a capability not permitted by a normal (default) mutex. However, its use requires more careful programming. For instance, if a recursively locked mutex were used with a condition variable, the unlock performed for a pthread_cond_wait() or pthread_cond_timedwait() would not actually release the mutex. In that case, no other thread can satisfy the condition of the predicate, and the thread would wait indefinitely. See Section 2.4.2 for information on the condition variable wait and timed wait routines.

2.4.1.4 Errorcheck Mutex

An errorcheck mutex is locked exactly once by a thread, like a normal mutex. If a thread tries to lock the mutex again without first unlocking it, the thread receives an error. If a thread other than the owner tries to unlock an errorcheck mutex, an error is returned. Thus, errorcheck mutexes are more informative than normal mutexes because normal mutexes deadlock in such a case, leaving you to determine why the thread no longer executes. Errorcheck mutexes are useful during development and debugging. Errorcheck mutexes can be replaced with normal mutexes when the code is put into production use, or left to provide the additional checking.

Errorcheck mutexes may be slower than normal mutexes, because they do more internal tracking. The debugger can always display the current owner (if any) of an errorcheck mutex. Any correct program that works with normal mutexes will also work with errorcheck mutexes.

2.4.1.5 Mutex Operations

To lock a mutex, use one of the following routines, depending on what you want to happen if the mutex is already locked by another thread:

When a thread is finished accessing a piece of shared data, it unlocks the associated mutex by calling the pthread_mutex_unlock() routine. If other threads are waiting on the mutex, one is placed in the ready state. If more than one thread is waiting on the mutex, the scheduling policy (see Section 2.3.2.2) and the scheduling priority (see Section 2.3.2.3) determine which thread is readied, and the next running thread that requests it locks the mutex.

The mutex is not automatically granted to the first waiter. If a running unlocking thread attempts to relock the mutex before the first waiter gets a chance to run, the running thread will succeed in relocking the mutex, and the first waiter may be forced to reblock.

You can destroy a mutex---that is, reclaim its storage---by calling the pthread_mutex_destroy() routine. Use this routine only after the mutex is no longer needed by any thread. It is invalid to attempt to destroy a mutex while it is locked or has threads waiting on it.

Warning

The Threads Library does not currently detect deadlock conditions involving more than one mutex, but may in the future. Never write code that depends upon the Threads Library not reporting a particular error condition.

2.4.1.6 Mutex Attributes

A mutex attributes object allows you to specify values other than the defaults for mutex attributes when you initialize a mutex with the pthread_mutex_init() routine.

The mutex type attribute specifies whether a mutex is default, normal, recursive, or errorcheck. Use the pthread_mutexattr_settype() routine to set the mutex type attribute in an initialized mutex attributes object. Use the pthread_mutexattr_gettype() routine to obtain the mutex type from an initialized mutex attributes object.

If you do not use a mutex attributes object to select a mutex type, calling the pthread_mutex_init() routine initializes a normal (default) mutex by default.

The process-shared attribute specifies whether a mutex can be operated upon by threads in only one process or by threads in more than one process, as follows:

2.4.2 Condition Variables

A condition variable is a synchronization object used in conjunction with a mutex. It allows a thread to block its own execution until some shared data object reaches a particular state. A mutex controls access to shared data; a condition variable allows threads to wait for that data to enter a defined state.

The state is defined by a predicate in the form of a Boolean expression. A predicate may be a Boolean variable in the shared data or the predicate may be indirect: for example, testing whether a counter has reached a certain value, or whether a queue is empty.

Each predicate should have its own unique condition variable. Sharing a single condition variable between more than one predicate can introduce inefficiency or errors unless you use extreme care.

Cooperating threads test the predicate and wait on the condition variable while the predicate is not in the desired state. For example, one thread in a program produces work-to-do packets and another thread consumes these packets (does the work). If there are no work-to-do packets when the consumer thread checks, that thread waits on a work-to-do condition variable. When the producer thread produces a packet, it signals the work-to-do condition variable.

You must associate a mutex with a condition variable. You may have multiple condition variables associated with the same mutex---representing different states of the same data---but you cannot use the same condition variable with multiple mutexes.

A thread uses a condition variable as follows:

  1. A thread locks a mutex for some shared data and then tests the relevant predicate. If it is not in the proper state, the thread waits on a condition variable associated with the predicate. Waiting on the condition variable automatically unlocks the mutex. It is essential that the mutex be unlocked, because another thread needs to acquire the mutex in order to put the data in the state required by the waiting thread.
  2. When the thread that acquires the mutex puts the data in the appropriate state, it wakes a waiting thread by signaling or broadcasting the condition variable.
  3. One thread (for signal), or all threads (for broadcast), comes out of the condition wait state. The threads always resume execution one at a time, because each thread must resume with the mutex locked (the condition wait relocks the mutex before returning from the thread). Other threads waiting on the condition variable remain blocked. Other threads awakened by a broadcast will block on the mutex until it is unblocked.

When a thread waits on a condition variable, it cannot assume that the predicate for which it is waiting will be satisfied when the condition variable wait returns. There are a number of reasons for this behavior. For instance, condition variable waits may return spuriously, meaning that the return may not be directly due to some other thread signaling or broadcasting the condition variable.

There are two reasons for these rules:

  1. It can be extremely expensive, especially on a symmetric multiprocessor (SMP) system, for the implementation to ensure that a condition signal wakes one and only one thread. It is faster and easier to avoid the extra complication. Also, a thread awakened by the delivery of a UNIX signal will return from the condition wait when the signal is dismissed; the wait predicate may not have changed.
  2. Spurious wakeups promote good programming practices. It may often be difficult to guarantee that a predicate will be true; most often, it is easy to determine that it might be true. It is often the case that, after signaling one thread that a predicate is true, another thread may manipulate the data in such a way that the predicate will not be true by the time the signaled thread runs. The solution is to recheck the predicate after the wait returns.

It is important to wait on the condition variable and evaluate the predicate in a while loop. This ensures that the program checks the predicate after it returns from the condition wait.

For example, a thread A may need to wait for a thread B to finish a task X before thread A proceeds to execute a task Y. Thread B can tell thread A that it has finished task X by putting a TRUE or FALSE value in a shared variable (the predicate). When thread A is ready to execute task Y, it looks at the shared variable to see if thread B is finished (see Figure 2-5).

Figure 2-5 Thread A Waits on Condition Ready


First, thread A locks the mutex named mutex_ready that is associated with the shared variable named ready. Then it reads the value in ready and compares it against some expected value. This test is called the predicate. If the predicate indicates that thread B has finished task X, then thread A can unlock the mutex and proceed with task Y. However, if the predicate indicates that thread B has not yet finished task X, then thread A waits for the predicate to change by calling the pthread_cond_wait() routine. This automatically unlocks the mutex, allowing thread B to lock the mutex when it has finished task X. Thread B updates the shared data (predicate) to the state thread A is waiting for and signals the condition variable by calling the pthread_cond_signal() routine (see Figure 2-6).

Figure 2-6 Thread B Signals Condition Ready


Thread B releases its lock on the shared variable's mutex. As a result of the signal, thread A wakes up, implicitly regaining its lock on the condition variable's mutex. It then verifies that the predicate is in the correct state, and proceeds to execute task Y (see Figure 2-7).

Figure 2-7 Thread A Wakes and Proceeds


Note that although the condition variable is used for communication among threads, the communication is anonymous. Thread B does not necessarily know that thread A is waiting on the condition variable that thread B signals, and thread A does not know that it was thread B that awakened it from its wait on the condition variable.

Use the pthread_cond_init() routine to initialize a condition variable. To create condition variables as part of your program's one-time initialization code, see Section 3.8. You can also statically initialize extern or static condition variables using the PTHREAD_COND_INITIALIZER macro provided in the pthread.h header file.

Use the pthread_cond_wait() routine to cause a thread to wait until the condition is signaled or broadcast. This routine specifies a condition variable and a mutex that you have locked. If you have not locked the mutex, the results of pthread_cond_wait() are unpredictable.

The pthread_cond_wait() routine automatically unlocks the mutex and causes the calling thread to wait on the condition variable until another thread calls one of the following routines:

If a thread signals or broadcasts a condition variable and there are no threads waiting at that time, the signal or broadcast has no effect. The next thread to wait on that condition variable blocks until the next signal or broadcast. (Alternatively, the nonportable pthread_cond_signal_int_np() routine creates a pending wake condition, which causes the next wait on the condition variable to complete immediately.)

If you want to limit the time that a thread waits on a condition variable, use the pthread_cond_timedwait() routine. This routine specifies the condition variable, mutex, and absolute time at which the wait should expire if the condition variable has not been signaled or broadcast.

You can destroy a condition variable and reclaim its storage by calling the pthread_cond_destroy() routine. Use this routine only after the condition variable is no longer needed by any thread. A condition variable cannot be destroyed while one or more threads are waiting on it.

2.4.3 Condition Variable Attributes

A condition variable attributes object allows you to specify values other than the defaults for condition variable attributes when you initialize a condition variable with the pthread_cond_init() routine.

The process-shared attribute specifies whether a condition variable can be operated upon by threads in only one process or by threads in more than one process, as follows:

2.4.4 Read-Write Locks

A read-write lock is a synchronization object for protecting shared data that can be accessed concurrently by more than one of the program's threads. Unlike a mutex, a read-write lock distinguishes between shared read and exclusive write operations on the shared data object.

Use a read-write lock to protect shared data that is read frequently but less frequently modified. For example, when you build a cache of recently accessed information, many threads might simultaneously examine the cache without conflict, but when a thread must update the cache it must have exclusive access.

When a thread locks a read-write lock, it must specify either shared read access or exclusive write access. Many threads may simultaneously acquire a read-write lock for read access, as long as there are no threads waiting for write access. A thread that wants read access cannot acquire the read-write lock if any thread has already acquired the read-write lock for write access; such a thread will block (wait) on the read-write lock. A thread trying to acquire the read-write lock for write access cannot continue if another thread has already acquired the read-write lock for either write access or read access; such a thread will block (wait) on the read-write lock.

2.4.4.1 Thread Priority and Writer Precedence for Read-Write Locks

If more than one thread is waiting for read access to a read-write lock, when the lock becomes available all of the threads will acquire the lock for read access.

If more than one thread is waiting for write access to a read-write lock, when the lock becomes available the thread in that group with the highest priority will acquire the lock for write access.

If both reader threads and writer threads are waiting for access to a read-write lock at the time the lock becomes available, one of the writer threads will acquire the lock, and the threads waiting for read access will continue to block.

The Threads Library implements "writer precedence" for read-write locks. A thread cannot acquire a read-write lock for read access if at least one thread is waiting for write access, even if other threads currently have read access. When a read-write lock is released, a waiting writer will be released if there are any, rather than releasing any waiting readers. Because readers usually outnumber writers, and read access occurs more frequently, writer precedence is needed to avoid "starvation". Without writer precedence, it would be possible that the read-write lock was always locked for read access, and writers would never run.

2.4.4.2 Initializing and Destroying a Read-Write Lock

Use the pthread_rwlock_init() routine to create and initialize a new read-write lock object.

Use the pthread_rwlock_destroy() routine to destroy a previously initialized read-write lock object.

You can initialize an extern or static read-write lock object using the PTHREAD_RWLOCK_INITIALIZER macro provided in the pthread.h header file.

2.4.4.3 Read-Write Lock Attributes

By default, a new read-write lock object's attributes have default values. To create a new read-write lock object with nondefault attributes, call the pthread_rwlock_init() routine and specify a read-write lock attributes object. Use the pthread_rwlockattr_init() routine to create a new read-write lock attributes object, and use the pthread_rwlockattr_destroy() routine to destroy a read-write lock attributes object.

There is one settable attribute for a read-write lock object, the process-shared attribute. To set and access the value of the process-shared attribute of a read-write lock attributes object, use the pthread_rwlockattr_getpshared() and pthread_rwlockattr_setpshared() routines, respectively.


Previous Next Contents Index

  [Go to the documentation home page] [How to order documentation] [Help on this site] [How to contact us]  
  privacy and legal statement  
6101PRO_005.HTML