BDE 4.14.0 Production release
Loading...
Searching...
No Matches
bslmt_qlock

Detailed Description

Outline

Purpose

Provide small, statically-initializable mutex lock.

Classes

See also
bslmt_mutex, bslmt_atomictypes, bslmt_lockguard, bslmt_once

Description

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:

| QLock | Mutex | SpinLock
-----------------------------------+-------+-------+---------
Memory footprint | small | large | small
Cost of construction/destruction | cheap | costly| cheap
Statically initializable | yes | no | yes
Speed at low contention | fast | fast | fast
Speed at high contention | slow | fast | very slow
Suitable for long critical regions | yes | yes | no
Fair | yes | no | no

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.

The bslmt::QLockGuard Class

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:

void lock(); // Acquire the lock, waiting if necessary
int tryLock(); // Acquire the lock if possible. Fail if lock is in use.
void unlock(); // Free the lock.

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:

void Node::update()
{
bslmt::QLockGuard qguard(&d_qlock); // 'd_qlock' is a 'bslmt::QLock'.
readLunarState();
if (d_moonIsFull) {
// Free lock while we sleep
sleep(TWENTY_FOUR_HOURS);
}
// Lock has been re-acquired
...
}
Definition bslmt_lockguard.h:297
Definition bslmt_qlock.h:381

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).

Usage

This section illustrates intended use of this component.

Example 1: Using bslmt::QLock to Implement a Thread-Safe Singleton

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:

const bsl::string& helloString()
{
Definition bslstl_string.h:1281

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:

static const bsl::string *singletonPtr = 0;
#define BSLMT_QLOCK_INITIALIZER
Definition bslmt_qlock.h:258
Definition bslmt_qlock.h:271

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:

bslmt::QLockGuard qlockGuard(&qlock);

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 .)

if (! singletonPtr) {
static bsl::string singleton("Hello");
singletonPtr = &singleton;
}

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.

return *singletonPtr;
}

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.

int usageExample1()
{
const bsl::string EXPECTED("Hello");
const bsl::string& hello1 = helloString();
assert(hello1 == EXPECTED);
const bsl::string& hello2 = helloString();
assert(hello2 == EXPECTED);
assert(&hello2 == &hello1);
return 0;
}