Application Development Guide --Core Components


Synchronization Objects

In a multithreaded program, you must use synchronization objects whenever there is a possibility of corruption of shared data or conflicting scheduling of threads that have mutual scheduling dependencies. The following subsections discuss two kinds of synchronization objects: mutexes and condition variables.

Mutexes

A mutex (mutual exclusion) is an object that multiple threads use to ensure the integrity of a shared resource that they access, most commonly shared data. A mutex has two states: locked and unlocked. 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 is blocked when it tries to lock the mutex if you call pthread_mutex_lock() (see Figure 9). The blocked thread continues and is not blocked if you call pthread_mutex_trylock().

Figure 9. Only One Thread Can Lock a Mutex



Figure a3u2j842 not displayed.

Each mutex must be initialized. (To initialize mutexes as part of the program's one-time initialization code, see "One-Time Initialization Routines".) To initialize a mutex, use the pthread_mutex_init() routine. This routine allows you to specify an attributes object, which allows you to specify the mutex type. The following are types of mutexes:

  • A fast mutex (the default) is locked only once by a thread. If the thread tries to lock the mutex again without first unlocking it, the thread waits for itself to release the first lock and deadlocks on itself.

    This type of mutex is called fast because it can be locked and unlocked more rapidly than a recursive mutex. It is the most efficient form of 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. Recursive mutexes have the notion of a mutex owner. When a thread 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 0 (zero). It is an error for any thread other than the owner to attempt to unlock the mutex.

    A recursive mutex is useful if a thread needs exclusive access to a piece of data, and it needs to call another routine (or itself) that needs exclusive access to the data. A recursive mutex allows nested attempts to lock the mutex to succeed rather than deadlock.

    This type of mutex requires more careful programming. Never use a recursive mutex with condition variables because the implicit unlock performed for a pthread_cond_wait() or pthread_cond_timedwait() may not actually release the mutex. In that case, no other thread can satisfy the condition of the predicate.

  • A nonrecursive mutex is locked only once by a thread, like a fast mutex. If the thread tries to lock the mutex again without first unlocking it, the thread receives an error. Thus, nonrecursive mutexes are more informative than fast mutexes because fast mutexes block in such a case, leaving it up to you to determine why the thread no longer executes. Also, if someone other than the owner tries to unlock a nonrecursive mutex, an error is returned.

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

  • The pthread_mutex_lock() routine

    If the mutex is locked, the thread waits for the mutex to become available.

  • The pthread_mutex_trylock() routine

    If the mutex is locked, the thread continues without waiting for the mutex to become available. The thread immediately checks the return status to see if the lock was successful, and then takes whatever action is appropriate if it was not.

When a thread is finished accessing a piece of shared data, it unlocks the associated mutex by calling the pthread_mutex_unlock() routine.

If another thread is waiting on the mutex, its execution is unblocked. If more than one thread is waiting on the mutex, the scheduling policy and the thread scheduling priority determine which thread acquires the mutex. (See "Scheduling Priority Attribute" for additional information.)

You can delete a mutex and reclaim its storage by calling the pthread_mutex_destroy() routine. Use this routine only after the mutex is no longer needed by any thread. This routine may require serialization. Mutexes are automatically deleted when the program terminates.

Note: Never include DCE APIs (such as pthread_mutex_destroy()) in the termination routine of a DLL. Doing so can result in an error (such as return code 6 -- invalid handle) when termination occurs out of sequence.

Condition Variables

A condition variable allows a thread to block its own execution until some shared data reaches a particular state. Cooperating threads check the shared data and wait on the condition variable. For example, one thread in a program produces work-to-do packets and another thread consumes these packets (does the work). If the work queue is empty when the consumer thread checks it, that thread waits on a work-to-do condition variable. When the producer thread puts a packet on the queue, it signals the work-to-do condition variable.

A condition variable is used to wait for a shared resource to assume some specific state (a predicate). A mutex, on the other hand, is used to reserve some shared resource while the resource is being manipulated. 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 using a variable they both have access to, a condition variable called a predicate. When thread A is ready to execute task Y, it looks at the condition variable (predicate) to see if thread B is finished (see Figure 10).

Figure 10. Thread A Waits on Condition Ready, Then Wakes Up and Proceeds



Figure a3u2j373 not displayed.

First, thread A locks the mutex named mutex_ready that is associated with the condition variable. Then it reads the predicate associated with the condition variable named ready. If the predicate indicates that thread B has finished task X, then thread A can unlock the mutex and proceed with task Y. If the condition variable predicate indicated that thread B has not yet finished task X; however, then thread A waits for the condition variable to change. Thread A calls the pthreadwait primitive. Waiting on the condition variable automatically unlocks the mutex, allowing thread B to lock the mutex when it has finished task X. The lock is automatically reacquired before waking up thread A(see Figure 11).

Figure 11. Thread B Signals Condition Ready



Figure a3u2j579 not displayed.

Thread B updates the predicate named ready associated with the condition variable to the state thread A is waiting for. It also executes a signal on the condition variable while holding the mutex mutex_ready.

Thread A wakes up, verifies that the condition variable (predicate) is in the correct state, and proceeds to execute task Y (see Figure 10).

Note that, although the condition variable is used for explicit communications among threads, the communications are 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 woke it up from its wait on the condition variable.

Use the pthread_cond_init() routine to create a condition variable. To create condition variables as part of the program's one-time initialization code, see "One-Time Initialization Routines".

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.) This routine unlocks the mutex and causes the calling thread to wait on the condition variable until another thread calls one of the following routines:

  • The pthread_cond_signal() routine to wake one thread that is waiting on the condition variable

  • The pthread_cond_broadcast() routine to wake all threads that are waiting on a condition variable

If you want to limit the time that a thread waits for a condition to be signaled or broadcast, 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 is not signaled or broadcast.

You can delete 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. Condition variables are automatically deleted when the program terminates.

Other Synchronization Methods

There is another synchronization method that is not anonymous: the join primitive. This allows a thread to wait for another specific thread to complete its execution. When the second thread is finished, the first thread unblocks and continues its execution. Unlike mutexes and condition variables, the join primitive is not associated with any particular shared data.

Posted by '김용환'
,