A.4 Mutexes
Edgar Dijkstra has proven that in the presence of multithreading, the thread scheduler of the operating system must provide certain synchronization objects. Without them, writing correct multithreaded applications is impossible.
Mutexes are fundamental synchronization objects that allow threads to access shared resources in an ordered manner. This section defines the notion of a mutex. The rest of Loki does not use mutexes directly; instead, it defines higher-level means of synchronization that can be easily implemented with mutexes.
Mutex is a collocation of Mutual Exclusive, a phrase that describes the functioning of this primitive object: A mutex allows threads mutually exclusive access to a resource.
The basic functions of a mutex are Acquire and Release. Each thread that needs exclusive access to a resource (such as a shared variable) acquires the mutex. Only one thread can acquire the mutex. After one thread acquires it, all other threads that invoke Acquire block in a wait state (the function Acquire does not return). When the thread that owns the mutex calls the Release function, the thread scheduler chooses one of the threads that is in a wait state on the same mutex and gives that thread the ownership of the mutex.
The observable effect is that mutexes are access serialization devices: The portion of code between a call to mtx.Acquire() and a call to mtx.Release() is atomic with respect to the mtx object. Any other attempt to acquire the mtx object must wait until the atomic operation finishes.
It follows that you should allocate one mutex object for each resource you want to share between threads. The resources you might want to share include, notably, C++ objects. Every nonatomic operation with these resources must start with acquiring the mutex and end with releasing the mutex. The nonatomic operations that you might want to perform include, notably, non-const member functions of thread-safe objects.
For instance, imagine you have a BankAccount class that provides functions such as Deposit and Withdraw. These operations do more than add to and subtract from a double member variable; they also log additional information regarding the transaction. If BankAccount is to be accessed from multiple threads, the two operations must certainly be atomic. Here's how you can do this:
class BankAccount
{
public:
void Deposit(double amount, const char* user)
{
mtx_.Acquire();
... perform deposit transaction ...
mtx_.Release();
}
void Withdraw(double amount, const char* user)
{
mtx_.Acquire();
... perform withdrawal transaction ...
mtx_.Release();
}
...
private:
Mutex mtx_;
...
};
As you probably have figured out (if you didn't know already), failing to call Release for each Acquire you issue has deadly effects. You lock the mutex and leave it locked— all other threads trying to acquire it block forever. In the previous code, you must implement Deposit and Withdraw very carefully with regard to exceptions and premature returns.
To mitigate this problem, many C++ threading APIs define a Lock object that you can initialize with a mutex. The Lock object's constructor calls Acquire, and its destructor calls Release. This way, if you allocate a Lock object on the stack, you can count on correct pairing of Acquire and Release, even in the presence of exceptions.
For portability reasons, Loki does not define mutexes on its own. It's likely you already use a multithreading library that defines its own mutexes. It would be awkward to duplicate their functionality. Instead, Loki relies on higher-level locking semantics that are implemented in terms of mutexes.
|