Chapter 24. A pool of locks that use event file descriptors

Index

Lock pool starvation option
Read-write lock pools
Mutually-exclusive lock pools

The x::lockpool template implements a thread-safe locking mechanism that uses an event file descriptor for signaling. The template takes several parameters, with the type of the lock identifier being the only required parameter:

#include <x/lockpool.H>

typedef x::lockpool<std::string> mylockpool_t;

mylockpool_t mylockpool(mylockpool_t::create());

This example defines mylockpool_t as a reference to a reference-counted object that implements locking based on text strings. That is, a lock is identified by a simple text string:

x::eventfd evfd(x::eventfd::create());

mylockpool_t::lockentry myLock(mylockpool->addLockSet("filelock", evfd));

while (!myLock->locked())
{
    evfd->event();
}

Acquiring a lock involves invoking the lock pool object's addLockSet() method, that takes a lock identifier and an event file descriptor. addLockSet() returns an opaque lockentry, which is a reference. addLockSet() does not wait until the requested lock has been acquired, it always returns immediately.

Use the lockentry's locked() method to check if the lock has been acquired. In this example, if another thread already holds a lock named filelock, locked() returns false until the existing lock gets released. The lock pool may simultaneously hold other locks with different names; locked() checks only that the given lock has been acquired. If not, the thread may then wait for an event to be registered on the given event file descriptor.

The lock, once acquired, exists as long as the lockentry instance remains in scope. The lock gets released when this lockentry reference goes out of scope. When the lock gets released, any thread that's waiting — for the same lock identifier — acquires it.

In the given example, only one lock gets acquired at a time. Multiple locks must be acquired one at a time. This is allowed. A single thread may hold more than one lockentry, however the application is responsible for avoiding deadlocks between multiple threads that are attempting to acquire locks held by each other. x::lockpool cannot detect deadlocks.

It is possible to define a x::lockpool that is capable of acquiring multiple locks simultaneously, where the first parameter to addLockSet() is a container of some sorts, that may hold multiple lock identifiers. The resulting lockentry's locked() method returns false as long as any lock identifier is already locked. Only when all lock identifiers in the lock set are not lock, the lock identifiers, in the lock set, get acquired simultaneously. The x::multilockpool template provides a convenient way to define lock pools where multiple lock identifiers get acquired at once.

Note that it's still possible to end up in a deadlock situation, where conflicting threads attempt to acquire lock sets that contain lock identifiers that are already held by each other. However, with the ability to acquire multiple locks at the same time, the need for a single thread to acquire multiple lock sets diminishes, and it should be possible to use logic with each individual application thread needing to acquire only one lockentry at a time, which eliminates any possibility for deadlocking.

Lock pool starvation option

The fourth, optional, parameter to the lockpool defaults to true. A waiting lock gets acquired at the earliest possible opportunity, as soon as a blocking lock gets removed from the lock pool. This — depending on the comparison function, and whether or not the lock pool implements multiple lock acquisition — may result in older waiting locks getting continuously starved by newer locks jumping ahead of them; as well as a lengthy scan of a long list of waiting locks, searching for any lock that can be acquired at this time.

Explicitly setting the starve flag to false sets a strict first-come/first-serve lock acquisition order. If the oldest waiting lock cannot be acquired because it's blocked, any newer locks are not checked. This results in faster performance and no starvation, however it means that a newer lock gets placed on the waiting list even when it is not blocked by another existing lock, because it must wait its turn behind an older waiting lock.

See the reference documentation for x::lockpool for more information.