<Mutex>

std::mutex

The mutex class is a synchronization primitive that can be used to protect shared data from being simutaneously accessed by multiple threads.

mutex offers exclusive, non-recursive ownership semantics:

- A calling thread owns a mutex from the time that it successfully call either lock or try_lock until it calls unlock.

- When a thread owns a mutex, all other threads will block (for calls to `lock`) or receive a `false` return value (for `try_lock`) if they attempt to claim ownership of the mutex.

- A calling thread must not own the mutex prior to calling `lock` or `try_lock`

The behavior of a program is undefined if a mutex is destoryed while still owned by any threads, or a thread terminates while owning a mutex. The mutex class satisfies all requirements of Mutex and StandardLayoutType.

std::mutex is neither copyable nor movable.

  • Member functions
name remark
(constructor) constructs the mutex
(destructor) destorys the mutex
operator=[delete] not copy-assignable
lock locks the mutex, blocks if the mutex is not available
try_lock tries to lock the mutex returns if the mutex is not available
unlock unlocks the mutex
native_handle returns the underlying implementation-defined native handle object

Notes: std::mutex is usually not accessed directly: std::unique_lock, std::lock_guard, or std::scoped_lock (since C++17) manage locking in a more exception-safe manner.

  • Example:

This example shows how a mutex can be used to prptect an std::map shared between two threads.

#include <chrono>
#include <iostream>
#include <map>
#include <mutex>
#include <mutex>
#include <string>
#include <thread>

std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex;

void save_page(const std::string& url) {
    // simulate a long page fetch
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::string result = "fake content";

    std::lock_guard<std::mutex> guard(g_pages_mutex);
    g_pages[url] = result;
}

int main() {
    std::thread t1(save_page, "http://foo");
    std::thread t2(save_page, "http://bar");
    t1.join();
    t2.join();

    // safe to access g_pages without lock now, as the threads are joined
    for (const auto& [url, page] : g_pages) {
        std::cout << url << " => " << page << "\n";
    }
}

// Output
// http://bar => fake content
// http://foo => fake content

std::lock_guard (c++11)

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

When a lock_guard object is created, it attempts to take ownership of the mutex it is givem. When control leaves the scope in which the lock_guard object was created, the lock_guard is destructed and the mutex is released.

The lock_guard class is non-copyable.

  • Template parameters

    mutex - the type of the mutex to lock. The type must meet the BasicLockable requirements.

  • Member types

Member type definition
mutex_type mutex
  • Member functions
name remarks
(constructor) constructs a lock_guard, optionally locking the given mutex
(destructor) destructs the lock_guard object, unlocks the underlying mutex
operator=[deleted] not copy-assignable

A common beginner error is to forget to give a lock_guard variable a name, such as by std::lock_guard(mtx). This constructs a prvalue object that is immediately destoryed, thereby not actually constructing a lock that holds a mutex for the rest of the scope.

scoped_lock ooers an alternative for lock_guard that provides the ability to lock mutiple mutexes using a deadlock avoidance algorithm.

  • Example
#include <iostream>
#include <mutex>
#include <string_view>
#include <syncstream>
#include <thread>

volatile int g_i = 0;
std::mutex g_i_mutex; // protects g_i

void safe_increment(int iterations) {
    const std::lock_guard<std::mutex> lock(g_i_mutex);
    while (iterations-- > 0)
        g_i = g_i + 1;
    std::cout << "thread #" << std::this_thread::get_id() << ", g_i: " << g_i << '\n';

    // g_i_mutex is automatically released when lock goes out of scope;
}

void unsafe_increment(int iterations) {
    while (iterations-- > 0)
        g_i = g_i + 1;
    std::osyncstream(std::cout) << "thread #" << std::this_thread::get_id() << ", g_i: " << g_i << '\n';
}

int main() {
    auto test = [](std::string_view fun_name, auto fun) {
        g_i = 0;
        std::cout << fun_name << ":\nbefore, g_i: " << g_i << '\n';
        {
            std::jthread t1(fun, 1'000'000);
            std::jthread t2(fun, 1'000'000);
        }
        std::cout << "after, g_i: " << g_i << "\n\n"
    };
    test("safe_increment", safe_increment);
    test("unsafe_increment", unsafe_increment);
}

// Possible output
/**
 *  safe_increment:
    before, g_i: 0
    thread #139158968334016, g_i: 1000000
    thread #139158959941312, g_i: 2000000
    after, g_i: 2000000

    unsafe_increment:
    before, g_i: 0
    thread #139158968334016, g_i: 1024970
    thread #139158959941312, g_i: 1024970
    after, g_i: 1024970
 */

std::unique_lock (c++11)

The class unique_lock is a general-purpose mutex ownership wrapper allowing deferred locking, time-constrained attempts at locking, recursive locking, transfer of lock ownership, and use with condition variables.

The class unique_lock is movable, bu now copyable – It meets the requirements of MoveConstructible and MoveAssignable but not of CopyConstructible or CopyAssignable.

The class unique_lock meets the BasicLockable requirements. If Mutex meets the Lockable requirements, unique_lock also meets the Lockable requirements (ex.: canbe used in std::lock); If Mutex meetts the TimedLockable requirements, unique_lock also meets the TimedLockable requirements.

  • Template parameters

Mutex - the type of the mutex to lock. The type must meet the BasicLockable requirements

  • Nexted types
Type Definition
mutex_type Mutex
  • Member functions
Name Remarks
(constructor) constructs a unique_lock, optionally locking (i.e., taking ownership of) the supplied mutex
(destructor) unlocks (i.e., releases ownership of) the associated mutex, if owned
operator= unlocks (i.e., releases ownership of) the mutex, if owned, and acquires ownership of another
lock locks (i.e, takes ownership of) the associated mutex
try_lock tries to lock (i.e, takes ownership of) the associated mutex without blocking
try_lock_for attempts to lock (i.e., takes ownership of) the associated TimedLockable mutex, returns if the mutex has been unavailable for the specified time duration
try_lock_until tries to lock (i.e., takes ownership of) assiciated TimedLockable mutex, returns if the mutex has been unavailable until specified time point has been reached
unlock unlocks (i.e., ownership of) the associated mutex
swap swaps state with anpther std::unique_lock
release disassociates the associated mutex without unlocking (i.e., releasing ownership of) it
mutex returns a pointer to the associated mutex
owns_lock tests whether the lock owns (i.e., has locked) its associated mutex
operator bool tests whether the lock owns (i.e., has locked) its associated mutex
  • Non-member functions

std::swap - specializes the std::swap algorithm

  • Example
#include <iostream>
#include <mutex>
#include <thread>

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_lcks 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";

    return 0;
}

// Output
// acc1: 95
// acc2: 55

std::scoped_lock

template< class... MutexTypes >
class scoped_lock

The class scoped_lock is a mutex wrapper that provides a convenient RAII mechanism for owning zero or mo mutexes for the duration of a scoped block.

When a scoped_lock object is created, it attempts to take ownership of the mutexes it is given. When control leaves the scope in which the scoped_lock object was created, the scoped_lock is destructed and the mutexes are released. If several mutexes are given, deadlock avoidance algorithm is used as if by std::lock

The scoped_lock class is non-copyable.

  • Template parameters

MutexTypes - the types of the mutexes to lock. The types must meet the Lockable requirements unless sizeof...(MutexTypes) == 1, in which case the only type must meet basicLockable

  • Member types
Member type Definition
mutex_type if sizeof...(MutexTypes) == 1, member type mutex_type is the same as Mutex, the sole type in MutexTypes…Other wise, there is no member mutex_type
  • Member functions
Name Remarks
(constructor) constructs a scoped_lock, optionally locking the given mutexes
(destructor) destructs the scoped_lock object, unlocks the underlying mutexes
operator=[delete] not copy-assignable
  • Note

A common beginner error is to “forget” to give a scoped_lock variable a name, e.g. std::scoped_lock(mtx); (which default constructs a scoped_lock variable named mtx) or std::scoped_lock{mtx}; (which constructs a prvalue object that is immediately destoryed), thereby not acturally constructing a lock that holds a mutex for the rest of the scope.

  • Examle

The following example uses std::scoped_lock to lock pairs of mutexes without deadlock and is RAII-style

#include <chrono>
#include <functional>
#include <iostream>
#include <mutex>
#include <string>
#include <syncstream>
#include <thread>
#include <vector>

using namespace std::chrono_literals;

struct Employee {
  std::vector<std::string> lunch_partners;
  std::string id;
  std::mutex m;
  Employee(std::string id) : id(id) {}
  std::string partners() const {
    std::string ret = "Employee " + id + " has lunch partners: ";
    for (int count{}; const auto &partner : lunch_partners) {
      ret += (count++ ? ", " : "") + partner;
    }
    return ret;
  }
};

void send_mail(Employee &, Employee &) { std::this_thread::sleep_for(1s); }

void assign_lunch_partner(Employee &e1, Employee &e2) {
  std::osyncstream synced_out(std::cout);
  synced_out << e1.id << " and " << e2.id << " are waiting for locks"
             << std::endl;

  {
    // Use std::scoped_lock to acquire two locks without worrying about
    // other calls to assign_lunch_partner deadlocking us
    // and it also provides a convenient RAII-style mechanism
    std::scoped_lock lock(e1.m, e2.m);

    // Equivalent code 1 (using std::lock and std::lock_guard)
    // std::lock(e1.m, e2.m);
    // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
    // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);

    // Equivalent code 2 (if unique_locks are needed, e.g. for condition
    // variables) std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
    // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
    // std::lock(lk1, lk2);
    synced_out << e1.id << " and " << e2.id << " got locks" << std::endl;
    e1.lunch_partners.push_back(e2.id);
    e2.lunch_partners.push_back(e1.id);
  }

  send_mail(e1, e2);
  send_mail(e2, e1);
}

int main() {
  Employee alice("Alice"), bob("Bob"), christina("Christina"), dave("Dave");

  // Assign in parallel threads because mailing users about lunch assignments
  // takes a long time
  std::vector<std::thread> threads;
  threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
  threads.emplace_back(assign_lunch_partner, std::ref(christina),
                       std::ref(bob));
  threads.emplace_back(assign_lunch_partner, std::ref(christina),
                       std::ref(alice));
  threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));

  for (auto &thread : threads) {
    thread.join();
  }
  std::osyncstream(std::cout) << alice.partners() << "\n"
                              << bob.partners() << "\n"
                              << christina.partners() << "\n"
                              << dave.partners() << "\n";
}

// Possible output
/**
 *  Christina and Alice are waiting for locks
    Christina and Alice got locks
    Dave and Bob are waiting for locks
    Dave and Bob got locks
    Christina and Bob are waiting for locks
    Christina and Bob got locks
    Alice and Bob are waiting for locks
    Alice and Bob got locks
    Employee Alice has lunch partners: Bob, Christina
    Employee Bob has lunch partners: Alice, Christina, Dave
    Employee Christina has lunch partners: Alice, Bob
    Employee Dave has lunch partners: Bob
 */

RAII

RAII can be summarized as follows:

  • encapsulate each resource into a class, where

    • the constructor acquires the resource and establishes all class invariants (类不变量) or throws an exception if that cannot be done.

    • the destructor releases the resource and never throws exceptions;

  • always use the resource via ana instance of a RAII-class that either

    • has automatic storage duration or temporary lifetime itself, or

    • has lifetime that is bounded by the lifetime of an automatic or temporary object.

Move semantics enable the transfer of resources and ownership between objects, inside and outside containers, and across threads, while ensuring resource safety.

Classes with open()/close(), lock()/unlock(), or init()/copyFrom()/destory() member functions are typical examples of non-RAII classes:

#include <mutex>

std::mutex m;

void bad() {
    m.lock(); // acqiuire the mutex
    f();      // if f() throws an exception, the mutex is never released
    if (!everything_ok()) {
        return; // early return, the mutex is never released
    }
    m.unlock(); // if bad() reaches this statement, the mutex is released
}

void good() {
    std::lock_guard<std::mutex> lk(m); // RAII class: mutex acquisition is initialization
    f();                               // if f() throws an exception, the mutex is released
    if (!everything_ok()) {
        return;                        // early return, the mutex is released
    }                                  // if good() returns normally, the mutex is released
}

Reference

  1. Mutex

  2. lock_guard

  3. unique_lock

  4. RAII