A Closer Look at QLock
Table 1 lists the QLock class data members, while Table 2 lists the values that d_readyFlag and d_nextFlag may have. The constructor QLock::QLock atomically exchanges the mutex pointer for this. If the mutex pointer was 0 prior to the exchange, the lock is obtained and the constructor completes. If the mutex pointer was not zero, this instance of QLock has to join the queue and wait for the ready flag.
Data Member | Description |
d_mutex | Pointer to the mutex locked by this QLock instance. |
d_next | Pointer to the next instance of QLock in queue waiting to obtain the lock. |
d_readyFlag | Flag used to indicate that the lock has been released by the predecessor. |
d_nextFlag | Flag used to indicate that the next pointer (d_next) has been set by a successor. |
Value | Description |
0 | Initial value indicating that the flag is not set. |
-1 | Indicates that the flag was set before an event object had been installed into the flag. |
Neither 0 nor -1 | Indicates that an event object has been installed into the flag, which now holds the handle of the event object. |
The destructor QLock::~QLock, using one atomic compare-and-exchange operation, checks if the mutex pointer contains the value of this pointer. If it does, it resets the mutex pointer to zero, thus releasing the mutex. If the mutex no longer contains the value of this pointer, the queue has been formed, and the destructor must pass the lock to the next instance in the queue. The destructor first waits on the d_nextFlag to make sure that the next pointer has been set, then sets the d_readyFlag.
The algorithms used in QLock's constructor/destructor are basically the same as those used to lock/unlock MCS spin-locks. The setFlag and waitOnFlag methods are where we make our important deviation from MCS locks. Instead of spinning (as is appropriate for a spin-lock), the waitOnFlag routine:
- Creates an event object.
- Atomically checks that the flag has not yet been set and installs the event object's handle into the flag.
- Proceeds to wait on the event object.
Now compare our QLock-based solution for the Singleton problem with the two previous solutions (spin-lock-based and named-mutex-based). Similar to the named-mutex-based solution, you avoid the unpredictable behavior of the spin-lock: Instead of spinning, waiting threads are suspended by the kernel, and resumed as soon as the mutex is available. Additionally, you avoid the problems with the named mutex: Event objects used for synchronization are process local, do not require artificial names, and are created only when threads need to be suspended due to contention. When there is no contention (when only one thread enters the Singleton::instance method at the time of initialization), no synchronization objects are created: The overhead is simply that of a few atomic operations.