C++ 通用锁管理
lock_guard
类 lock_guard 是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。
创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。
lock_guard 类不可复制。
成员类型
mutex_type Mutex成员函数
成员函数
(构造函数) 构造 lock_guard ,可选地锁定给定的互斥(公开成员函数)
(析构函数) 析构 lock_guard 对象,解锁底层互斥(公开成员函数)
operator=[被删除] 不可复制赋值(公开成员函数)
#include <thread>
#include <mutex>
#include <iostream>
int g_i = 0;
std::mutex g_i_mutex; // 保护 g_i
void safe_increment()
{
std::lock_guard<std::mutex> lock(g_i_mutex);
++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
// g_i_mutex 在锁离开作用域时自动释放
}
int main()
{
std::cout << "main: " << g_i << '\n';
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
}
可能的输出:
main: 0
140641306900224: 1
140641298507520: 2
main: 2
scoped_lock
类 scoped_lock 是提供便利 RAII 风格机制的互斥包装器,它在作用域块的存在期间占有一或多个互斥。
创建 scoped_lock 对象时,它试图取得给定互斥的所有权。控制离开创建 scoped_lock 对象的作用域时,析构 scoped_lock 并释放互斥。若给出数个互斥,则使用免死锁算法,如同以 std::lock 。
scoped_lock 类不可复制。
成员类型
mutex_type (若 sizeof...(MutexTypes)==1) Mutex , MutexTypes... 中的单独类型
成员函数
(构造函数) 构造 scoped_lock ,可选地锁定给定的互斥(公开成员函数)
(析构函数) 析构 scoped_lock 对象,解锁底层互斥(公开成员函数)
operator=[被删除] 不可复制(公开成员函数)
#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <functional>
#include <chrono>
#include <string>
struct Employee
{
Employee(std::string id) : id(id) {}
std::string id;
std::vector<std::string> lunch_partners;
std::mutex m;
std::string output() const
{
std::string ret = "Employee " + id + " has lunch partners: ";
for (const auto &partner : lunch_partners)
ret += partner + " ";
return ret;
}
};
void send_mail(Employee &, Employee &)
{
// 模拟耗时的发信操作
std::this_thread::sleep_for(std::chrono::seconds(1));
}
void assign_lunch_partner(Employee &e1, Employee &e2)
{
static std::mutex io_mutex;
{
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
}
{
// 用 std::scoped_lock 取得二个锁,而无需担心
// 其他对 assign_lunch_partner 的调用死锁我们
// 而且它亦提供便利的 RAII 风格机制
std::scoped_lock lock(e1.m, e2.m);
// 等价代码 1 (用 std::lock 和 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);
// 等价代码 2 (若需要 unique_lock ,例如对于条件变量)
// 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);
{
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << 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::cout << alice.output() << '\n'
<< bob.output() << '\n'
<< christina.output() << '\n'
<< dave.output() << '\n';
}
可能的输出:
alice and bob are waiting for locks
alice and bob got locks
christina and bob are waiting for locks
christina and alice are waiting for locks
dave and bob are waiting for locks
dave and bob got locks
christina and alice got locks
christina and bob got locks
Employee alice has lunch partners: bob christina
Employee bob has lunch partners: alice dave christina
Employee christina has lunch partners: alice bob
Employee dave has lunch partners: bob
unique_lock
类 unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。
类 unique_lock 可移动,但不可复制——它满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。
类 unique_lock 满足基本可锁定 (BasicLockable) 要求。若 Mutex 满足可锁定 (Lockable) 要求,则 unique_lock 亦满足可锁定 (Lockable) 要求(例如:能用于 std::lock ) ;若 Mutex 满足可定时锁定 (TimedLockable) 要求,则 unique_lock 亦满足可定时锁定 (TimedLockable) 要求。
成员类型
mutex_type Mutex
成员函数
(构造函数) 构造 unique_lock ,可选地锁定提供的互斥(公开成员函数)
(析构函数) 若占有关联互斥,则解锁之(公开成员函数)
operator= 若占有则解锁互斥,并取得另一者的所有权(公开成员函数)
锁定
lock 锁定关联互斥(公开成员函数)
try_lock 尝试锁定关联互斥,若互斥不可用则返回(公开成员函数)
try_lock_for 试图锁定关联的可定时锁定 (TimedLockable) 互斥,若互斥在给定时长中不可用则返回(公开成员函数)
try_lock_until 尝试锁定关联可定时锁定 (TimedLockable) 互斥,若抵达指定时间点互斥仍不可用则返回(公开成员函数)
unlock 解锁关联互斥(公开成员函数)
修改器
swap 与另一 std::unique_lock 交换状态(公开成员函数)
release 将关联互斥解关联而不解锁它(公开成员函数)
观察器
mutex 返回指向关联互斥的指针(公开成员函数)
owns_lock 测试锁是否占有其关联互斥(公开成员函数)
operator bool 测试锁是否占有其关联互斥(公开成员函数)非成员函数
std::swap(std::unique_lock) (C++11) std::swap 对 unique_lock 的特化(函数模板)
#include <mutex>
#include <thread>
#include <chrono>
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<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
// 锁两个 unique_lock 而不死锁
std::lock(lock1, lock2);
from.num_things -= num;
to.num_things += num;
// 'from.m' 与 'to.m' 互斥解锁于 'unique_lock' 析构函数
}
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();
}
shared_lock
类 shared_lock 是通用共享互斥所有权包装器,允许延迟锁定、定时锁定和锁所有权的转移。锁定 shared_lock ,会以共享模式锁定关联的共享互斥( std::unique_lock 可用于以排他性模式锁定)。
shared_lock 类可移动,但不可复制——它满足可移动构造 (MoveConstructible) 与可移动赋值 (MoveAssignable) 的要求,但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。
shared_lock 符合可锁定 (Lockable) 要求。若 Mutex 符合可共享定时锁定 (SharedTimedLockable) 要求,则 shared_lock 亦符合 可定时锁定 (TimedLockable) 要求。
为以共享所有权模式等待于共享互斥,可使用 std::condition_variable_any ( std::condition_variable 要求 std::unique_lock 故而只能以唯一所有权模式等待)。
成员类型
mutex_type Mutex
成员函数
(构造函数) 构造 shared_lock ,可选地锁定提供的互斥(公开成员函数)
(析构函数) 解锁关联的互斥(公开成员函数)
operator= 若占有则解锁互斥,然后获得对方的所有权(公开成员函数)
共享锁定
lock 锁定关联的互斥(公开成员函数)
try_lock 尝试锁定关联的互斥(公开成员函数)
try_lock_for 尝试锁定关联的互斥,以指定时长(公开成员函数)
try_lock_until 尝试锁定关联的互斥,直至指定的时间点(公开成员函数)
unlock 解锁关联的互斥(公开成员函数)
修改器
swap 与另一 shared_lock 交换数据成员(公开成员函数)
release 解除关联 mutex 而不解锁(公开成员函数)
观察器
mutex 返回指向关联的互斥的指针(公开成员函数)
owns_lock 测试锁是否占有其关联的互斥(公开成员函数)
operator bool 测试锁是否占有其关联的互斥(公开成员函数)
非成员函数
std::swap(std::shared_lock)(C++14) std::swap 对 shared_lock 的特化(函数模板)
lock
锁定给定的可锁定 (Lockable) 对象 lock1 、 lock2 、 ... 、 lockn ,用免死锁算法避免死锁。
以对 lock 、 try_lock 和 unlock 的未指定系列调用锁定对象。若调用 lock 或 unlock 导致异常,则在重抛前对任何已锁的对象调用 unlock 。
#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <functional>
#include <chrono>
#include <string>
struct Employee {
Employee(std::string id) : id(id) {}
std::string id;
std::vector<std::string> lunch_partners;
std::mutex m;
std::string output() const
{
std::string ret = "Employee " + id + " has lunch partners: ";
for( const auto& partner : lunch_partners )
ret += partner + " ";
return ret;
}
};
void send_mail(Employee &, Employee &)
{
// 模拟耗时的发信操作
std::this_thread::sleep_for(std::chrono::seconds(1));
}
void assign_lunch_partner(Employee &e1, Employee &e2)
{
static std::mutex io_mutex;
{
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
}
// 用 std::lock 获得二个锁,而不担心对 assign_lunch_partner 的其他调用会死锁我们
{
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);
// 等价代码(若需要 unique_locks ,例如对于条件变量)
// 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);
// C++17 中可用的较优解法
// std::scoped_lock lk(e1.m, e2.m);
{
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << 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::cout << alice.output() << '\n' << bob.output() << '\n'
<< christina.output() << '\n' << dave.output() << '\n';
}
可能的输出:
alice and bob are waiting for locks
alice and bob got locks
christina and bob are waiting for locks
christina and bob got locks
christina and alice are waiting for locks
christina and alice got locks
dave and bob are waiting for locks
dave and bob got locks
Employee alice has lunch partners: bob christina
Employee bob has lunch partners: alice christina dave
Employee christina has lunch partners: bob alice
Employee dave has lunch partners: bob
std::defer_lock, std::try_to_lock, std::adopt_lock
-
std::lock_guard的std::adopt_lock参数
- 加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();
- adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock()。
-
unique_lock的第二个参数std::adopt_lock:
- 表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了。
- 前提:必须提前lock
-
std::try_to_lock:
- 尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;
- 使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方
- 前提:不能提前lock();
- owns_locks()方法判断是否拿到锁,如拿到返回true
-
std::defer_lock:
- 如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex
- 不给它加锁的目的是以后可以调用unique_lock的一些方法
- 前提:不能提前lock
call_once
函数模板,该函数的第一个参数为标记,第二个参数是一个函数名。
功能:能够保证函数只被调用一次。具备互斥量的能力,而且比互斥量消耗的资源更少,更高效。
call_once()需要与一个标记结合使用,这个标记为std::once_flag;其实once_flag是一个结构,call_once()就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态。
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag1, flag2;
void simple_do_once()
{
std::call_once(flag1, []()
{ std::cout << "Simple example: called once\n"; });
}
void may_throw_function(bool do_throw)
{
if (do_throw)
{
std::cout << "throw: call_once will retry\n";
// throw std::exception();
}
std::cout << "Didn't throw, call_once will not attempt again\n";
}
void do_once(bool do_throw)
{
try
{
std::call_once(flag2, may_throw_function, do_throw);
}
catch (...)
{
}
}
int main()
{
std::thread st1(simple_do_once);
std::thread st2(simple_do_once);
std::thread st3(simple_do_once);
st1.join();
st2.join();
st3.join();
std::thread t1(do_once, true);
std::thread t2(do_once, false);
std::thread t3(do_once, true);
std::thread t4(do_once, true);
t1.join();
t2.join();
t3.join();
t4.join();
}
可能会是:
Simple example: called once
throw: call_once will retry
Didn't throw, call_once will not attempt again