This page describes how to use mutexes — one of the available synchronization primitives.
On this page:
Mutex Member Functions
IceUtil::Mutex (defined in
IceUtil/Mutex.h) provides a simple non-recursive mutual exclusion mechanism:
The member functions of this class work as follows:
You can optionally specify a mutex protocol when you construct a mutex. The mutex protocol controls how the mutex behaves with respect to thread priorities. Default-constructed mutexes use a system-wide default.
lockfunction attempts to acquire the mutex. If the mutex is already locked, it suspends the calling thread until the mutex becomes available. The call returns once the calling thread has acquired the mutex.
tryLockfunction attempts to acquire the mutex. If the mutex is available, the call returns true with the mutex locked. Otherwise, if the mutex is locked by another thread, the call returns false.
unlockfunction unlocks the mutex.
IceUtil::Mutex is a non-recursive mutex implementation. This means that you must adhere to the following rules:
- Do not call
lockon the same mutex more than once from a thread. The mutex is not recursive so, if the owner of a mutex attempts to lock it a second time, the behavior is undefined.
- Do not call
unlockon a mutex unless the calling thread holds the lock. Calling
unlockon a mutex that is not currently held by any thread, or calling
unlockon a mutex that is held by a different thread, results in undefined behavior.
IceUtil::RecMutex class if you need recursive semantics.
Adding Thread Safety to the File System Application in C++
Recall that the implementation of the
write operations for our file system server is not thread safe:
The problem here is that, if we receive concurrent invocations of
write, one thread will be assigning to the
_lines vector while another thread is reading that same vector. The outcome of such concurrent data access is undefined; to avoid the problem, we need to serialize access to the
_lines member with a mutex. We can make the mutex a data member of the
FileI class and lock and unlock it in the
FileI class here is identical to the original implementation, except that we have added the
_fileMutex data member. The
write operations lock and unlock the mutex to ensure that only one thread can read or write the file at a time. Note that, by using a separate mutex for each
FileI instance, it is still possible for multiple threads to concurrently read or write files, as long as they each access a different file. Only concurrent accesses to the same file are serialized.
The implementation of
read is somewhat awkward here: we must make a local copy of the file contents while we are holding the lock and return that copy. Doing so is necessary because we must unlock the mutex before we can return from the function. However, as we will see in the next section, the copy can be avoided by using a helper class that unlocks the mutex automatically when the function returns.
Guaranteed Unlocking of Mutexes in C++
Using the raw
unlock operations on mutexes has an inherent problem: if you forget to unlock a mutex, your program will deadlock. Forgetting to unlock a mutex is easier than you might suspect, for example:
Assume that we are keeping the contents of the file on secondary storage, such as a database, and that the
readFileContents function accesses the file. The code is almost identical to the previous example but now contains a latent bug: if
readFileContents throws an exception, the
read function terminates without ever unlocking the mutex. In other words, this implementation of
read is not exception-safe.
The same problem can easily arise if you have a larger function with multiple return paths. For example:
In this example, the early return from the middle of the function leaves the mutex locked. Even though this example makes the problem quite obvious, in large and complex pieces of code, both exceptions and early returns can cause hard-to-track deadlock problems. To avoid this, the
Mutex class contains two type definitions for helper classes, called
TryLockT are simple templates that primarily consist of a constructor and a destructor; the
LockT constructor calls
lock on its argument, and the
TryLockT constructor calls
tryLock on its argument. The destructors call
unlock if the mutex is locked when the template goes out of scope. By instantiating a local variable of type
TryLock, we can avoid the deadlock problem entirely:
This is an example of the RAII (Resource Acquisition Is Initialization) idiom .
On entry to
someFunction, we instantiate a local variable
lock, of type
IceUtil::Mutex::Lock. The constructor of
lock on the mutex so the remainder of the function is inside a critical region. Eventually,
someFunction returns, either via an ordinary return (in the middle of the function or at the end) or because an exception was thrown somewhere in the function body. Regardless of how the function terminates, the C++ run time unwinds the stack and calls the destructor of
lock, which unlocks the mutex, so we cannot get trapped by the deadlock problem we had previously.
TryLock templates have a few member functions:
void acquire() const
This function attempts to acquire the lock and blocks the calling thread until the lock becomes available. If the caller calls
acquireon a mutex it has locked previously, the function throws
bool tryAcquire() const
This function attempts to acquire the mutex. If the mutex can be acquired, it returns true with the mutex locked; if the mutex cannot be acquired, it returns false. If the caller calls
tryAcquireon a mutex it has locked previously, the function throws
void release() const
This function releases a previously locked mutex. If the caller calls release on a mutex it has unlocked previously, the function throws
bool acquired() const
This function returns true if the caller has locked the mutex previously, otherwise it returns false. If you use the
TryLocktemplate, you must call
acquiredafter instantiating the template to test whether the lock actually was acquired.
These functions are useful if you want to use the
TryLock templates for guaranteed unlocking, but need to temporarily release the lock:
You should make it a habit to always use the
TryLock helpers instead of calling
unlock directly. Doing so results in code that is easier to understand and maintain.
Lock helper, we can rewrite the implementation of our
write operations as follows:
Note that this also eliminates the need to make a copy of the
_lines data member: the return value is initialized under protection of the mutex and cannot be modified by another thread once the destructor of
lock unlocks the mutex.
- Stroustrup, B. 1997. The C++ Programming Language. Reading, MA: Addison-Wesley.