The following synchronization classes each implement some form of mutual exclusion:
An RWMutexLock implements a form of synchronization called mutual exclusion, where the presence of one thread within a protected or critical section of code prohibits the entry of other threads into that section.
A thread must acquire ownership of a mutex prior to entering the section of code protected by the mutex. This is accomplished by calling the acquire() method of the mutex. Once a thread is granted ownership of the mutex, it is allowed to proceed into the protected code. Other threads attempting to enter this same section of code also attempt to acquire ownership of the mutex, but these threads are forced to wait until the current owner exits the protected section of code and releases ownership of the mutex.
A thread relinquishes its ownership of a mutex by calling its release() method.
To avoid blocking during the acquisition of a mutex that is already owned by another thread, use the tryAcquire() method. This function returns FALSE if the mutex is already owned by another thread and returns TRUE if the mutex has been successfully acquired.
The interactions between these functions are illustrated using the interaction diagram shown in Figure 24:
In some environments, RWMutexLock supports timed acquisition of a mutex. This capability is included in a version of the acquire() function that accepts a time limit for waiting, specified as an unsigned long number of milliseconds.
In those environments that do not support a timed wait on a mutex, the timed version of acquire() attempts a conditional acquisition of the mutex, and if the mutex is not immediately available, the function returns a value indicating timeout. The macro RW_THR_HAS_TIMED_MUTEX_ACQUIRE can be used to determine whether or not the current environment supports true timeouts for mutex acquisition; this macro is defined when timed waits are supported.
RWMutexLock does not support recursive acquisition. In a debug version of the Synchronization package, any attempt to recursively acquire a mutex produces an assertion that aborts the process. In the release version, a recursive acquisition can succeed or can result in deadlock. The specific behavior is defined by the underlying threads system. If recursive acquisition semantics are required, use the RWTRecursiveLock template class to create a mutex that allows for it.
If the order in which threads acquire a mutex are the same order in which ownership is granted, then first-in-first-out or FIFO ordering is being used. The RWMutexLock class does not ensure FIFO ordering.
Different systems use different policies for granting mutex ownership. Some prioritize acquisition requests based on the priority of the thread; others can maintain FIFO ordering (unless the thread is interrupted for some reason, in which case its position in any internal queue can be changed).
If you require FIFO acquisition semantics regardless of a thread's priority or any other factors, then use the RWFIFOMutexLock class.
An RWMutexLock is commonly used to protect data or resources by limiting access to one thread at a time. Access is typically controlled by requiring that a mutex be acquired prior to reading or manipulating the data or resource. Once the actual access is completed, the mutex should then be released.
Example 35 illustrates this concept by implementing a simple thread-safe queue class based on a Tools.h++ linked list class.
#include <rw/sync/RWMutexLock.h> #include <rw/tvslist.h> template <class T> class Queue { private: RWMutexLock mutex_; RWTValSlist<T> list_; public: void enqueue(const T& t) { mutex_.acquire(); list_.append(t); mutex_.release(); } T dequeue(void) { mutex_.acquire(); T result = list_.removeFirst(t); mutex_.release(); return result; } }
Section 4.4.2, "Using Mutexes," discussed the disadvantages of explicitly coding the mutex acquisition and release, which was done in this example. If either linked-list member function or the copy constructor for the template parameter class throws an exception, the Queue's mutex is not released.
Try-catch blocks can be added to correct this problem, but it is much easier to use a guard class instance to automatically acquire and release the mutex at construction and destruction.
By using the lock guard defined by the RWMutexLock class, this example can be rewritten as in Example 36.
template <class T> class Queue { private: RWMutexLock mutex_; RWTValSlist<T> list_; public: void enqueue(const T& t) { RWMutexLock::LockGuard lock(mutex_); list_.append(t); } T dequeue(void) { RWMutexLock::LockGuard lock(mutex_); return list_.removeFirst(t); } }
In this version, the mutex is always released, even if an exception is thrown by one of the list members or by the template parameter class.
In addition to the public typedef for LockGuard, RWMutexLock also defines typedefs for UnlockGuard, TryLockGuard, ReadLockGuard, WriteLockGuard, TryReadLockGuard, TryWriteLockGuard, ReadUnlockGuard, and WriteUnlockGuard.
Normally, a mutex is initialized by its constructor before an acquire operation can occur. However, a mutex declared at global scope can be accessed before it is constructed, if threads are launched during the static initialization phase. Therefore, global static instances of RWMutexLock can require a different style of initialization.
To account for this possibility, the mutex acquire operation is designed to check for proper initialization each time it is called. If the mutex has not been initialized, the acquire operation performs the initialization itself. This allows a thread to safely acquire a mutex prior to its actual construction.
If a global mutex is declared using the normal constructor and that mutex is accessed before construction, then the mutex is initialized a second time when it is finally constructed.
To avoid the possibility of an erroneous second initialization, you must use a special constructor that does not initialize the mutex. This static constructor is selected by initializing the mutex with a special enumerated constant, RW_STATIC_CTOR, as shown in this declaration:
// Declare a global static mutex instance RWMutexLock mutex(RW_STATIC_CTOR);
The RWMutexLock static constructor must only be used for global static instances. Use of this constructor with an automatically or dynamically allocated instance produces errors or other unpredictable behavior.
NOTE: The initialization of a global static mutex is not thread-safe. It is conceivable that two threads can attempt to acquire and initialize a mutex simultaneously, resulting in a race condition. You must take care when designing your application to avoid such a possibility.
The synchronization classes can automatically service cancellation requests before performing any operation that can result in blocking the calling thread. If you want to do this, enable cancellation processing for the synchronization object when you construct it by passing the appropriate RWCancellationState value to the constructor.
The default cancellation state of a synchronization object is RW_CANCELLATION_DISABLED. You can override that default by specifying the Threading package RW_CANCELLATION_ENABLED state when you construct the synchronization instance, as shown in Example 37.
RWMutexLock mutex(RW_CANCELLATION_ENABLED); try { mutex.acquire(); // May throw cancellation! } catch(RWCancellation&) { // Do something about it! }
You can also enable and disable synchronization cancellation processing as shown in Example 38. This approach uses the enableCancellation() and setCancellation() member functions included in the RWSynchObject class, which is the base class for all synchronization mechanisms.
#include <rw/thread/RWCancellation.h> RWMutexLock mutex; // Defaults to disabled! mutex.setCancellation(RW_CANCELLATION_ENABLED); try { mutex.acquire(); // May throw cancellation! } catch(RWCancellation&) { // Do something about it! }
This example requires the Threading package. See Chapter 3, "The Threading Package," for more information on cancellation.
The RWFIFOMutexLock is a special form of mutex that preserves thread acquisition ordering when granting ownership of the mutex. Except for the guarantee of FIFO acquisition semantics, this class has the same methods and features as the RWMutexLock class and can be used in an identical fashion.
The RWNullMutexLock class is a special type of mutex that does nothing. It has the same interface as an RWMutexLock, but all of the functions are null. This feature is important when code is used in non-multithread situations because it saves execution time.
The RWTRecursiveLock template class converts other mutex classes, such as RWMutexLock or RWFIFOMutexLock, into a mutex that supports recursive acquisition semantics.
A thread acquires ownership of a recursive mutex the first time acquire() is called. Any subsequent calls to acquire() without an intervening call to release() cause an internal acquire-count value to be incremented. The internal count is used to ensure that a recursive mutex is released as many times as it was acquired before ownership is relinquished.
The RWTRecursiveLock template class is most useful for building monitors that allow one member function to call another member function in the same instance without producing deadlock.
Recursive locks must acquire a value that uniquely identifies a thread of execution. This value helps determine whether the recursive lock blocks or simply increments its count.
A thread "id" might also be required:
When you need to directly call the underlying API.
When doing your own thread identification within both your program logic and any debug or status output that you produce.
Thread identification is implemented by the following functions:
RWThreadId
rwThreadId()
RWThreadId
RWRunnableHandle::threadId() (in the Threading package)
The Synchronization package encapsulates the underlying thread id using the RWThreadId class. Instances of this class can be used just as if they are instances of the underlying thread id type. That's because the RWThreadId class defines constructors and conversion operators that allow it to be used in this manner. For more information about this class, see the Threads.h++ Reference Guide.
You can use the rwThreadId() function to retrieve the id of the current or calling thread. Any thread can call this function, not just threads created by the library.
In Example 39, the date function must lock the entire Date instance to prevent partial changes while the day, month, and year accessor functions acquire and release the mutex to protect their individual members. The recursive mutex allows the date function to use the day, month, and year accessors without producing deadlock.
class Date { private: RWTRecursiveLock<RWMutexLock> mutex_; unsigned int day_; unsigned int month_; int year_; ... public: unsigned int day() const { RWTRecursiveLock<RWMutexLock>::LockGuard lock( const_cast<Date*>(this)->mutex_); return day_; } void day(unsigned int newDay) { RWTRecursiveLock<RWMutexLock>::LockGuard lock( const_cast<Date*>(this)->mutex_); day_ = validateDay(newDay); } unsigned int month() const { RWTRecursiveLock<RWMutexLock>::LockGuard lock( const_cast<Date*>(this)->mutex_); return month_; } void month(unsigned int newMonth) { RWTRecursiveLock<RWMutexLock>::LockGuard lock( const_cast<Date*>(this)->mutex_); month_ = validateMonth(newMonth); } int year() const { RWTRecursiveLock<RWMutexLock>::LockGuard lock( const_cast<Date*>(this)->mutex_); return year_; } void year(int newYear) { RWTRecursiveLock<RWMutexLock>::LockGuard lock( const_cast<Date*>(this)->mutex_); year_ = validateYear(newYear); } RWCString date() const { RWTRecursiveLock<RWMutexLock>::LockGuard lock( const_cast<Date*>(this)->mutex_); char buf[11]; sprintf(buf,"%02d-%02d-%04d",month(),day(),year()); return RWCString(buf); } ... };
A readers-writer lock is a synchronization mechanism that combines mutual exclusion with condition synchronization to allow any number of readers or a single writer to acquire ownership of the lock. A readers-writer lock is useful in those situations where many readers exist for which mutual exclusion would unnecessarily reduce concurrency and where updates or writes occur infrequently when compared to reads.
The RWReadersWriterLock class interface has semantics similar to that of a mutex, but the functions have been renamed for separate read and write access. A thread requests read access by calling the acquireRead() method and requests write access by calling the acquireWrite() method. To release either type of access, the thread calls the lock's release() function.
To avoid blocking during the acquisition of a readers-writer lock that is already owned by another thread or threads, use the tryAcquireRead() and tryAcquireWrite() methods. These functions return FALSE if the lock is already exclusively owned by another thread and return TRUE if the lock has been successfully acquired.
The Synchronization package implementation of the readers-write lock prefers writers over readers when granting access.
The RWCriticalSection class is primarily intended for use in the Microsoft Win32 environment. In this environment, the class encapsulates the Win32 critical section mechanism, which is an efficient form of mutual exclusion based on a spin-lock. Spin-locks do not block a thread while waiting to acquire ownership of the critical section. A waiting thread spins in a polling loop until the section is released.
A critical section is more efficient than a mutex when little competition exists for a lock because the Win32 mutex acquisition requires kernel intervention even when the mutex is not currently owned by another thread.
In environments other than Win32, this class is implemented as a mutex. This allows a Win32 version of an application to realize the performance benefits of a critical section, but also allows that same code to be ported to platforms that do not have a critical section mechanism.
The semaphore class uses a unique form of synchronization that might be considered a cross between mutual exclusion and condition synchronization. It implements a classic counting semaphore as developed by E. W. Dijkstra.
A semaphore is basically an integer counter, initialized to zero by default, that can be incremented and decremented. If a thread attempts to decrement the count when it is already zero, the thread is blocked until the decrement operation can succeed. Any thread can increment the semaphore, potentially releasing a thread that has been blocked trying to decrement it.
The decrement and increment operations are performed by the acquire() and release() methods, respectively. These functions correspond to Dijkstra's P and V semaphore operations.
The RWSemaphore class has two versions of the acquire() function:
The first blocks indefinitely until the decrement operation can be performed.
The second accepts a time-limit for the wait, expressed as an unsigned long number of milliseconds.
When using an RWSemaphore instance that has been decremented to zero, you cannot assume that the order in which threads block trying to acquire is the same order that the threads unblock when the semaphore is released. Different systems can use different policies for processing semaphore acquisitions. Some can prioritize acquisition requests based on the priority of the thread; others can maintain a FIFO ordering (unless the thread is interrupted for some reason) in which case its position in any internal queue can be changed.
As explained in Section 4.4.5, another common form of thread synchronization is condition synchronization, which uses an implementation of a condition variable. To implement condition synchronization using a condition variable, you combine the functionality of RWCondition with the mutual exclusion capability of RWMutexLock.
When using an RWCondition instance to implement condition synchronization, a mutex is required to prohibit changes to the program state upon which the synchronization condition depends. A thread subjected to condition synchronization acquires the mutex, tests the condition, and if the condition is found to be false, calls the wait() function on the condition variable. This wait operation temporarily unlocks the mutex so other threads can access and update the program state.
A thread that is going to change the program state acquires the mutex, updates the state, and signals one waiting thread by calling the signal() function on the condition variable. When the waiting thread is awakened, the mutex is reacquired and control is returned to the thread.
Having exited the wait, a thread typically reevaluates the synchronization condition. If the condition is still false, the thread again waits. If the condition is true, the thread has achieved synchronization and can now release the mutex and proceed.
NOTE: The mutex must be acquired prior to calling the wait(), signal(), or signalAll() functions. Failure to do so produces assertions in the debug version of the library and can result in other errors in the release version.
The mutex can be acquired and released using its own member functions or the acquire() and release() member functions of the RWCondition class.
To simplify implementation, the RWCondition class includes both a mutex and a condition-variable interface. An RWCondition class instance does not possess its own mutex; you must supply a reference to an RWMutexLock instance when you construct each condition instance. Like the RWMutexLock class, RWCondition also has public typedefs for the lock guard classes.
The class definition in Example 40 demonstrates typical usage for the RWCondition class.
#include <rw/sync/RWMutexLock.h> #include <rw/sync/RWCondition.h> class ConditionMonitor { private: RWMutexLock mutex_; RWCondition condition_; // Declare other state variables here... RWBoolean state_; // Dummy state variable public: ConditionMonitor() : condition_(mutex_), // 1 state_(FALSE) { } RWBoolean waitForStateChange() { RWCondition::LockGuard lock(condition_); // 2 RWBoolean initial = state_; while(state_ == initial) { // 3 condition_.wait(); // 4 } return state_; } void changeState(RWBoolean newState) { RWCondition::LockGuard lock(condition_); state_ = newState; condition_.signalAll(); // 5 } . . . };
//1 | Initialize the condition with a reference to a mutex. This mutex must be acquired whenever the condition wait(), signal(), or signalAll() functions are called. |
//2 | Acquire a lock on the mutex using the condition variable's interface. |
//3 | Test for a change in state. |
//4 | Wait for another thread to signal that it has changed the state. |
//5 | Signal the change in state. |
The synchronization semantics of an RWCondition instance are illustrated in the interaction diagram Figure 25:
The RWBarrier class uses condition synchronization to implement another form of synchronization called a barrier or rendezvous. This mechanism synchronizes the activities of a group of two or more threads.
A barrier is initialized with the number of threads that it synchronizes. As each thread in the group completes some computational stage or activity, it calls the wait() method of the barrier, where it is blocked until the remaining threads have also entered the barrier. When the last thread calls wait(), all of the blocked threads are released.
Example 41 demonstrates the use of the barrier to coordinate the activities of a group of threads.
#include <rw/rstream.h> #include <rw/thread/rwtMakeThreadFunction.h> #include <rw/sync/RWBarrier.h> void heave_ho(RWBarrier& barrier, size_t pulls) { for (size_t i=0; i<pulls; i++) { cout << "Heave!" << endl; barrier.wait(); cout << "Ho!" << endl; barrier.wait(); } } void tugThatLine(size_t pulls) { const size_t count=5; // Number of sailors RWThread thread[count]; RWBarrier barrier(count); size_t i; // Create the threads for (i=0; i<count; i++) { thread[i] = rwMakeThreadFunctionGA2(void, heave_ho, RWBarrier&, barrier, size_t, pulls); } // Start the threads for (i=0; i<count; i++) thread[i].start(); // Wait for the threads to finish for (i=0; i<count; i++) thread[i].join(); } void main() { tugThatLine(3); // Ask for three good tugs! }
The output is:
Heave! Heave! Heave! Heave! Heave! Ho! Ho! Ho! Ho! Ho! . . . (for a total of three times)
The RWTMonitor class is used to build monitor objects. It is a simple template class that contains a mutex instance, and it is used as a base class for implementing C++ classes whose member functions have thread-safe access to the data or resources encapsulated by that class. See the example in Section 4.4.4, "Building Monitors," for more information about using this class.
©Copyright 2000, Rogue Wave Software, Inc.
Contact Rogue Wave about documentation or support issues.