mutex (C++)

std::lock (C++)

Locks the given Lockable objects lock1, lock2, …, lockn using a deadlock avoidance algorithm to avoid Deadlock.

void task_a () {
  // foo.lock(); bar.lock(); // replaced by:
  std::lock (foo,bar);
  std::cout << "task a\n";
  foo.unlock();
  bar.unlock();
}

Though generally, when we refer to locks in C++, we dont use the “naked” std::lock. The locks below employ RAII, which automatically lock and unlock a mutex. These are:

  • std::lock_guard
  • std::unique_lock
  • NEW std::scoped_lock

Resources

Mutex vs. Lock

Always prefer locks to mutexes, using a 1ock_guard

Motivation is here: https://www.modernescpp.com/index.php/prefer-locks-to-mutexes/

Locks take care of their resource following the RAII idiom. A lock automatically binds its mutex in the constructor and releases it in the destructor. This considerably reduces the risk of a deadlock because the runtime takes care of the mutex.

BAD

mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();
  • Easy to forget to unlock, easy to result in Deadlock

GOOD

std::mutex m,
std::lock_guard<std::mutex> lockGuard(m);
sharedVariable= getVar();

Types of locks

std::lock_guard

https://cplusplus.com/reference/mutex/lock_guard/

The class lock_guard is a mutex wrapper that provides a convenient RAII-style mechanism for owning a mutex for the duration of a scoped block.

// lock_guard example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard
#include <stdexcept>      // std::logic_error
 
std::mutex mtx;
 
void print_even (int x) {
  if (x%2==0) std::cout << x << " is even\n";
  else throw (std::logic_error("not even"));
}
 
void print_thread_id (int id) {
  try {
    // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:
    std::lock_guard<std::mutex> lck (mtx);
    print_even(id);
  }
  catch (std::logic_error&) {
    std::cout << "[exception caught]\n";
  }
}
 
int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_thread_id,i+1);
 
  for (auto& th : threads) th.join();
 
  return 0;
}

unique_lock

std::unique_lock is mightier but more expansive than its small brother std::lock_guard.

unique_lock locks in exclusive mode.

#include <mutex>
#include <thread>
#include <iostream>
 
struct Box
{
    explicit Box(int num) : num_things{num} {}
 
    int num_things;
    std::mutex m;
};
 
void transfer(Box &from, Box &to, int num)
{
    // don't actually take the locks yet
    std::unique_lock lock1{from.m, std::defer_lock};
    std::unique_lock lock2{to.m, std::defer_lock};
 
    // lock both unique_locks without deadlock
    std::lock(lock1, lock2);
 
    from.num_things -= num;
    to.num_things += num;
 
    // 'from.m' and 'to.m' mutexes unlocked in 'unique_lock' dtors
}
 
int main()
{
    Box acc1{100};
    Box acc2{50};
 
    std::thread t1{transfer, std::ref(acc1), std::ref(acc2), 10};
    std::thread t2{transfer, std::ref(acc2), std::ref(acc1), 5};
 
    t1.join();
    t2.join();
 
    std::cout << "acc1: " << acc1.num_things << "\n"
                 "acc2: " << acc2.num_things << '\n';
}

shared_lock

shared_lock allows deferred locking, timed locking and transfer of lock ownership.

https://en.cppreference.com/w/cpp/thread/shared_lock/shared_lock

#include <shared_mutex>
#include <syncstream>
#include <iostream>
#include <thread>
#include <chrono>
 
std::shared_timed_mutex m;
int i = 10;
 
void read_shared_var(int id)
{
   // both the threads get access to the integer i
   std::shared_lock<std::shared_timed_mutex> slk(m);
   const int ii = i; // reads global i
 
   std::osyncstream(std::cout) << "#" << id << " read i as " << ii << "...\n";
   std::this_thread::sleep_for(std::chrono::milliseconds(10));
   std::osyncstream(std::cout) << "#" << id << " woke up..." << std::endl;
}
 
int main()
{
   std::thread r1 {read_shared_var, 1};
   std::thread r2 {read_shared_var, 2};
 
   r1.join();
   r2.join();
}