BDE 4.14.0 Production release
|
Provide small, statically-initializable mutex lock.
This component defines a portable and efficient lock for ensuring that only one thread at a time enters a specific "critical region" – a section of code that accesses a shared resource – bslmt::Qlock
and its associated bslmt::QLockGuard
. The functionality of the bslmt::QLock
class overlaps those of the bslmt::Mutex
and bsls::SpinLock
classes, but with different usage and performance characteristics, as shown in the following grid:
The performance trade-offs for a QLock are quite different than those for a conventional mutex. QLocks are best suited for low-contention applications where large numbers of locks may be needed. For example, a node-based data structure that needs a lock for each node can benefit from the small size and low initialization cost of a QLock compared to that of a conventional mutex. A bslmt::Mutex
object cannot be initialized statically because some platforms (e.g., Windows XP) do not have a native statically-initializable mutex type. A bslmt::QLock
object, in contrast is statically initializable on all platforms.
The performance characteristics of a QLock are very similar to those of a SpinLock. However, a QLock is much more suitable than a SpinLock for situations where the critical region is more than a few instructions long. Also, although QLocks are best for low-contention situations, they do not degrade nearly as badly as SpinLocks if there is a lot of contention for the lock. They also use significantly fewer CPU cycles in high-contention situations than do SpinLocks.
A unique characteristic of QLocks is that they are fair. If there is contention for a lock, each thread is given the lock in the order in which it requested it. Consequently, every thread competing for the lock will get a chance before any other thread can have a second turn; no thread is ever "starved" out of the critical region. This fairness comes at a cost, however, in that the scheduler is given less leeway to schedule threads in the most efficient manner.
A bslmt::QLock
is different from other locking classes such as bslmt::Mutex
and bsls::SpinLock
in that it cannot be manipulated except through the auxiliary bslmt::QLockGuard
class. The reason for this limited interface is that a QLock requires a small amount of additional storage for each thread that is holding or waiting for the lock. The bslmt::QLockGuard
provides this extra storage efficiently on the stack.
In typical usage, a bslmt::QLockGuard
is created as a local (stack) variable, acquires the lock in its constructor and releases the lock in its destructor. If the lock is in use at construction time, then the current thread blocks until the lock becomes available. Although the QLock itself is intended to be shared among multiple threads, the guard object must never be used by more than one thread at a time. When multiple threads want to acquire the same QLock, each must use its own bslmt::QLockGuard
object.
bslmt::QLockGuard
also provides the following manipulators typical of locking classes:
As with other types of mutexes, only one thread may hold the lock at a time. Other threads attempting to call lock
will block until the lock becomes available. However, it is important to remember that the manipulators listed above are only pass-through operations on the shared bslmt::QLock
object. In other words, upon return from calling lock
on a bslmt::QLockGuard
object, a thread has actually acquired the lock to the underlying bslmt::QLock
.
Although it is only a proxy for the actual QLock, lock
/unlock
/tryLock
interface of bslmt::QLockGuard
allows it to be treated as though it were itself a lock. In particular, it is possible to instantiate the bslmt::LockGuard
and bslmt::LockGuardUnlock
class templates using bslmt::QLockGuard
. This layering of guard classes is useful for creating regions where the QLock is locked or unlocked. For example, if a thread acquires a QLock and then needs to temporarily relinquish it, it could use a bslmt::LockGuardUnlock
as follows:
The behavior is undefined if unlock
is invoked from a thread that did not successfully acquire the lock, or if lock
is called twice in a thread without an intervening call to unlock
(i.e., bslmt::QLockGuard
is non-recursive).
This section illustrates intended use of this component.
For this example, assume that we have the need to use the string "Hello" repeatedly in the form of an bsl::string
object. Rather than construct the string each time we use it, it would be nice to have only one copy so that we can amortize the memory allocation and construction cost over all the uses of the string. It is thus logical to have a single, static variable (a singleton) of type bsl::string
initialized with the value, "Hello". Unfortunately, as this is a multithreaded application, there is the danger that more than one thread will attempt to initialize the singleton simultaneously, causing a memory leak at best and memory corruption at worse. To solve this problem, we use a bslmt::QLock
to synchronize access to the singleton.
We begin by wrapping the singleton in a function:
This function defines two static variables, a pointer to the singleton, and a QLock to control access to the singleton. Note that both of these variables are statically initialized, so there is no need for a run-time constructor and hence no danger of a race condition among threads. The need for static initialization is the main reason we choose to use bslmt::QLock
over bslmt::Mutex
:
Before checking the status of the singleton pointer, we must make sure that we are not accessing the pointer at the same time that some other thread is modifying the pointer. We do this by acquiring the lock by constructing a bslmt::QLockGuard
object:
Now we are inside the critical region. If the pointer has not already been set, we can initialize the singleton knowing that no other thread is manipulating or accessing these variables at the same time. Note that this critical region involves constructing a variable of type bsl::string
. This operation, while not ultra-expensive, is too lengthy for comfortably holding a spinlock. Again, the characteristics of bslmt::QLock
are superior to the alternatives for this application. (It is worth noting that the QLock concept was created specifically to permit this kind of one-time processing. See also bslmt_once .)
Finally, we return a reference to the singleton. The destructor for bslmt::QLockGuard
will automatically unlock the QLock and allow another thread into the critical region.
The following test program shows how our singleton function can be called. Note that hello1
and hello2
have the same address, demonstrating that there was only one string created.