<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.
| 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.
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) {
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();
for (const auto& [url, page] : g_pages) {
std::cout << url << " => " << page << "\n";
}
}
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.
| Member type |
definition |
| mutex_type |
mutex |
| 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.
#include <iostream>
#include <mutex>
#include <string_view>
#include <syncstream>
#include <thread>
volatile int g_i = 0;
std::mutex g_i_mutex;
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';
}
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);
}
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.
Mutex - the type of the mutex to lock. The type must meet the BasicLockable requirements
| Type |
Definition |
| mutex_type |
Mutex |
| 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 |
std::swap - specializes the std::swap algorithm
#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) {
std::unique_lock lock1{from.m, std::defer_lock};
std::unique_lock lock2{to.m, std::defer_lock};
std::lock(lock1, lock2);
from.num_things -= num;
to.num_things += num;
}
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;
}
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.
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 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 |
| 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 |
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.
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;
{
std::scoped_lock lock(e1.m, e2.m);
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");
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";
}
RAII
RAII can be summarized as follows:
-
encapsulate each resource into a class, where
-
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();
f();
if (!everything_ok()) {
return;
}
m.unlock();
}
void good() {
std::lock_guard<std::mutex> lk(m);
f();
if (!everything_ok()) {
return;
}
}
Reference
-
Mutex
-
lock_guard
-
unique_lock
-
RAII