We are going to write a program that uses an std::shared_mutex instance in its exclusive and shared modes and to see what that means. Additionally, we do not call the lock and unlock functions ourselves but do the locking with automatic unlocking using RAII helpers:
- First, we need to include all necessary headers. Because we use STL functions and data structures all the time together with time literals, we declare that we use the std and chrono_literal namespaces:
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
using namespace std;
using namespace chrono_literals;
- The whole program revolves around one shared mutex, so let's define a global instance for the sake of simplicity:
shared_mutex shared_mut;
- We are going to use the std::shared_lock and std::unique_lock RAII helpers. In order to make their names appear less clumsy, we define short type aliases for them:
using shrd_lck = shared_lock<shared_mutex>;
using uniq_lck = unique_lock<shared_mutex>;
- Before beginning with the main function, we define two helper functions that both try to lock the mutex in exclusive mode. This function here will instantiate a unique_lock instance on the shared mutex. The second constructor argument defer_lock tells the object to keep the lock unlocked. Otherwise, its constructor would try to lock the mutex and then block until it succeeds. Then we call try_lock on the exclusive_lock object. This call will return immediately and its boolean return value tells us if it got the lock or if the mutex was locked already somewhere else:
static void print_exclusive()
{
uniq_lck l {shared_mut, defer_lock};
if (l.try_lock()) {
cout << "Got exclusive lock.n";
} else {
cout << "Unable to lock exclusively.n";
}
}
- The other helper function tries to lock the mutex in exclusive mode, too. It blocks until it gets the lock. Then we simulate some error case by throwing an exception (which carries just a plain integer number instead of a more complex exception object). Although this leads to an immediate exit of the context in which we hold a locked mutex, the mutex will cleanly be released again. That is because the destructor of unique_lock will release the lock in any case by design:
static void exclusive_throw()
{
uniq_lck l {shared_mut};
throw 123;
}
- Now to the main function. First, we open up another scope and instantiate a shared_lock instance. Its constructor immediately locks the mutex in shared mode. We will see what this means in the next steps:
int main()
{
{
shrd_lck sl1 {shared_mut};
cout << "shared lock once.n";
- Now we open yet another scope and instantiate a second shared_lock instance on the same mutex. We have two shared_lock instances now, and they both hold a shared lock on the mutex. In fact, we could instantiate arbitrarily many shared_lock instances on the same mutex. Then we call print_exclusive, which tries to lock the mutex in exclusive mode. This will not succeed because it is locked in shared mode already:
{
shrd_lck sl2 {shared_mut};
cout << "shared lock twice.n";
print_exclusive();
}
- After leaving the latest scope, the destructor of the shared_lock sl2 releases its shared lock on the mutex. The print_exclusive function will again fail because the mutex is still in shared lock mode:
cout << "shared lock once again.n";
print_exclusive();
}
cout << "lock is free.n";
- After leaving also the other scope, all shared_lock objects are destroyed, and the mutex is in unlocked state again. Now we can finally lock the mutex in exclusive mode. Let's do this by calling exclusive_throw and then print_exclusive. Remember that we throw an exception in exclusive_throw. But because unique_lock is an RAII object that gives us exception safety, the mutex will be unlocked again no matter how we return from exclusive_throw. This way print_exclusive will not block on an erroneously still locked mutex:
try {
exclusive_throw();
} catch (int e) {
cout << "Got exception " << e << 'n';
}
print_exclusive();
}
- Compiling and running the code yields the following output. The first two lines show that we got the two shared lock instances. Then the print_exclusive function fails to lock the mutex in exclusive mode. After leaving the inner scope and unlocking the second shared lock, the print_exclusive function still fails. After leaving the other scope too, which finally released the mutex again, exclusive_throw and print_exclusive are finally able to lock the mutex:
$ ./shared_lock
shared lock once.
shared lock twice.
Unable to lock exclusively.
shared lock once again.
Unable to lock exclusively.
lock is free.
Got exception 123
Got exclusive lock.