今天聊聊std::lock_guard/std::unique_lock,首先要说的是unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。平时也会使用到std::lock_guard,但是std::unique_lock用的比较少。因为std::lock_guard可以满足我们大部分需求。
还有一个关键的问题,我们已经有了std::mutex,为什么还要存在这两个东东呢?因为我们想用RAII风格的锁。首先我们要理解什么是RAII。
什么是RAII:
资源获取即初始化(Resource Acquisition Is Initialization),或称 RAII,是一种 C++ 编程技术,它将必须在使用前请求的资源(分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接等——任何存在受限供给中的事物)的生命周期绑定与一个对象的生存期相绑定。
RAII 保证资源可用于任何会访问该对象的函数(资源可用性是一种类不变式,这会消除冗余的运行时测试)。它亦保证所有资源在其控制对象的生存期结束时,以获取顺序的逆序释放。类似地,若资源获取失败(构造函数以异常退出),则为已构造完成的对象和基类子对象所获取的所有资源,会以初始化顺序的逆序释放。这有效地利用了语言特性(对象生存期、退出作用域、初始化顺序以及栈回溯)以消除内存泄漏并保证异常安全。根据 RAII 对象的生存期在退出作用域时结束这一基本状况,此技术的另一名称是作用域界定的资源管理( Scope-Bound Resource Management,SBRM)。
RAII 可总结如下:
-
将每个资源封装入一个类,其中
-
-
构造函数请求资源,并建立所有类不变式,或在它无法完成时抛出异常,
-
析构函数释放资源并决不抛出异常;
-
-
始终经由 RAII 类的实例使用满足要求的资源,该资源
-
-
自身拥有自动存储期或临时生存期,或
-
具有与自动或临时对象的生存期绑定的生存期
-
移动语义使得在对象间,跨作用域,以及在线程内外安全地移动所有权,而同时维护资源安全成为可能。
拥有 open()/close()、lock()/unlock(),或 init()/copyFrom()/destroy() 成员函数的类是非 RAII 类的典型的例子:
std::mutex m;
void bad()
{
m.lock(); // 请求互斥体
f(); // 若 f() 抛异常,则互斥体永远不被释放
if(!everything_ok()) return; // 提早返回,互斥体永远不被释放
m.unlock(); // 若 bad() 抵达此语句,互斥才被释放
}
void good()
{
std::lock_guard<std::mutex> lk(m); // RAII类:互斥体的请求即是初始化
f(); // 若 f() 抛异常,则释放互斥体
if(!everything_ok()) return; // 提早返回,互斥体被释放
} // 若 good() 正常返回,则释放互斥体
了解了他们存在的合理性,我们具体的介绍他们。
std::lock_guard
-
lock_guard定义于头文件 <mutex>
-
lock_guard是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。
-
创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。
-
lock_guard 类不可复制。
有了lock_guard,我们就可以写RAII风格的程序了,举个小例子:
#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
std::unique_lock
-
定义于头文件 <mutex>
-
类unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。
-
类unique_lock 可移动,但不可复制——它满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。
-
类 unique_lock 满足基本可锁定 (BasicLockable) 要求。
-
若 Mutex 满足可锁定 (Lockable) 要求,则 unique_lock 亦满足可锁定 (Lockable) 要求(例如:能用于 std::lock )
-
若 Mutex 满足可定时锁定 (TimedLockable) 要求,则 unique_lock 亦满足可定时锁定 (TimedLockable) 要求。
用人话说就是除了lock_guard的功能(也就是:类 unique_lock 满足基本可锁定 (BasicLockable) 要求),unique_lock 还可以锁定两种类型:可锁定 (Lockable),可定时锁定 (TimedLockable)。
锁定 | |
lock | 锁定关联互斥 (公开成员函数) |
try_lock | 尝试锁定关联互斥,若互斥不可用则返回 (公开成员函数) |
try_lock_for | 试图锁定关联的可定时锁定 (TimedLockable) 互斥,若互斥在给定时长中不可用则返回 (公开成员函数) |
try_lock_until | 尝试锁定关联可定时锁定 (TimedLockable) 互斥,若抵达指定时间点互斥仍不可用则返回 (公开成员函数) |
所以说unique_lock比lock_guard使用更加灵活,功能更加强大。
这里就不举所有的例子了,举一个try_lock的例子:
// unique_lock::try_lock example
#include <iostream> // std::cout
#include <vector> // std::vector
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock, std::defer_lock
std::mutex mtx; // mutex for critical section
void print_star () {
std::unique_lock<std::mutex> lck(mtx,std::defer_lock);
// print '*' if successfully locked, 'x' otherwise:
if (lck.try_lock())
std::cout << '*';
else
std::cout << 'x';
}
int main ()
{
std::vector<std::thread> threads;
for (int i=0; i<500; ++i)
threads.emplace_back(print_star);
for (auto& x: threads) x.join();
return 0;
}
可能的输出:
*****************************x******************************x*x***x***x*x*x**x**
x**********x********************************************************************
************x*x*x*x*************************************************************
*******x********x**********x****************************************************
***************************************x*x*x*x**x*x*x*x*x*x*********************
**x*****************************************************************************
***************x****