std::lock_guard
Exception Thrown in Critical Section
try {
task_mutex.lock(); // Lock the mutex before the critical section
// Critical section throws an exception
task_mutex.unlock(); // Never gets called
} catch (std::exception &e) {
...
}
}
-
The mutex will be left locked
-
When the exception is thrown:
- The destructors are called for all objects in scope
- The program flow jumps into the catch handler
- The unlock call is never executed
- The mutex remains locked
-
All other threads which are waiting to lock the mutex are blocked
-
If main() is joined on these blocked threads
- main() will be blocked as well
- The entire program is blocked
Drawbacks of std::mutex
-
Calling lock() requires a corresponding call to unlock()
- If not, the mutex will remain locked after the thread exits
-
unlock() must always be called, even if
- There are multiple paths through the critical section
- An exception is thrown
-
Relies on the programmer to get it right
-
For these reasons, we do not normally use
std::mutexdirectly
Mutex Wrapper Classes
-
The C++ Library provides mutex wrapper classes
- Classes with a mutex object as a private member
- Defined in
<mutex>
-
These use the RAII idiom for managing resources
- In this case, the resource is a lock on a mutex
- The constructor locks the mutex
- The destructor unlocks the mutex
-
We create the wrapper class on the stack
- The mutex will always be unlocked when the object goes out of scope
- Including when an exception is thrown
std::lock_guard
-
It is a very basic wrapper
- Has a constructor and destructor only
-
The constructor takes a mutex object as argument
- Initializes its member from the argument
- Locks it
-
The destructor unlocks the mutex member
-
std::lock_guardis a template class -
The template parameter is the type of the mutex
// Create a wrapper object for task_mutex
// which has type std::mutex
std::lock_guard<std::mutex> lck_guard(task_mutex);
- In C++17, the compiler can deduce the mutex's type
std::lock_guard lck_guard(task_mutex);
Output Example using std::lock_guard
- Do not explicitly lock the mutex
- Create an
std::lock_guardobject - Pass the mutex to its constructor
#include <exception>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
using namespace std::literals;
std::mutex print_mutex;
void task(std::string str) {
for (int i = 0; i < 5; ++i) {
try {
// Create an std::lock_guard object
// This calls task_mutex.lock()
std::lock_guard<std::mutex> lck_guard(print_mutex);
// Critical section
std::cout << str[0] << str[1] << str[2] << std::endl;
// throw std::exception();
// End of critical section
std::this_thread::sleep_for(50ms);
} // calls ~std::lock_guard
catch (std::exception &e) {
std::cout << "Exception caught: " << e.what() << '\n';
}
}
}
int main() {
std::thread thr1(task, "abc");
std::thread thr2(task, "def");
std::thread thr3(task, "xyz");
thr1.join();
thr2.join();
thr3.join();
}
- lck_guard is created, its constructor calls lock()
- lck_guard goes out of scope, its destructor calls unlock() -If an exception is thrown, lck_guard's destructor is called and unlocks the mutex
- The mutex is never left unlocked
- However, the mutex is still locked after the end of the critical section
- Other threads cannot lock the mutex until lck_guard is destroyed