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::mutex directly

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_guard is 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_guard object
  • 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