【C++11 多线程】lock_guard与unique_lock(五)
一、前言
如果g_mutex.lock()
和g_mutex.unlock()
之间的语句发生了异常,会发生什么?看这个例子:
#include <iostream>
#include <thread>
#include <mutex>
// 实例化互斥锁对象,不要理解为定义变量
std::mutex g_mutex;
// 使用锁保护,创建一个线程安全的打印函数
void safePrint(std::string msg, int val) {
g_mutex.lock(); // 上锁
std::cout << msg << val << std::endl;
// 模拟程序中的异常,实际异常要复杂的多
if (val == -2)
return;
g_mutex.unlock(); // 解锁
}
void threadTask()
{
for (int i = 0; i < 10; i++)
safePrint("print thread: ", i);
}
int main()
{
std::thread t(threadTask);
for (int i = 0; i > -10; i--)
safePrint("print main: ", i);
t.join();
return 0;
}
/*
输出:
print main: 0
print main: -1
print main: -2
然后就报错:abort() has been called
*/
g_mutex.unlock()
语句没有机会执行!导致导致g_mutex
一直处于锁着的状态,其他使用safePrint
函数的线程就会阻塞,甚至可能导致程序崩溃。
解决这个问题也很简单,使用 C++ 中常见的RAII
技术,即获取资源即初始化(Resource Acquisition Is Initialization)技术,这是 C++ 中管理资源的常用方式。简单的说就是在类的构造函数中创建资源,在析构函数中释放资源,这样就算发生了异常, C++ 也能保证类的析构函数能够执行。
我们不需要自己写个类包装mutex
, C++ 库已经提供了应用RAII
技术的std::lock_guard
类模板。
二、std::lock_guard
std::lock_guard
原理是:声明一个局部的std::lock_guard
对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:创建即加锁,作用域结束自动解锁。从而使std::lock_guard()
就可以替代lock()
与unlock()
。
void safePrint(std::string msg, int val) {
// 用此语句替换了g_mutex.lock(),参数为互斥锁g_mutex
std::lock_guard<std::mutex> guard(g_mutex);
std::cout << msg << val << std::endl;
} // 此时不需要写g_mutex.unlock(),guard出了作用域被释放,自动调用析构函数,于是g_mutex被解锁
需要互斥访问共享资源的那段代码称为临界区,临界区范围应该尽可能的小,即 lock 互斥量后应该尽早 unlock,通过使用 {} 来调整作用域范围,可使得互斥量 g_mutex 在合适的地方被解锁。
void safePrint(std::string msg, int val) {
{
// 用此语句替换了g_mutex.lock(),参数为互斥锁g_mutex
std::lock_guard<std::mutex> guard(g_mutex);
std::cout << msg << val << std::endl;
} // 通过使用{}来调整作用域范围,可使得g_mutex在合适的地方被解锁
std::cout << "作用域外的内容" << std::endl;
}
推荐使用std::lock_guard
,这样可以防止因为异常无法解锁或者程序员自己忘记解锁。
三、std::unique_lock
互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁。
这一点lock_guard
做的不好,不够灵活,lock_guard
只能保证在析构的时候执行解锁操作,lock_guard
本身并没有提供加锁和解锁的接口,但是有些时候会有这种需求。看下面的例子:
class LogFile {
std::mutex m_mutex;
std::ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void safePrint(std::string msg, int val) {
{
std::lock_guard<std::mutex> guard(m_mutex);
// do something 1
}
// do something 2
{
std::lock_guard<std::mutex> guard(m_mutex);
// do something 3
f << msg << val << std::endl;
std::cout << msg << val << std::endl;
}
}
};
上面的代码中,一个函数内部有两段代码需要进行保护,这个时候使用lock_guard
就需要创建两个局部对象来管理同一个互斥锁(其实也可以只创建一个,但是锁的粒度太大,效率不行),修改方法是使用unique_lock
。它提供了lock()
和unlock()
接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁(lock_guard
就一定会解锁)。上面的代码修改如下:
class LogFile {
std::mutex m_mutex;
std::ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void safePrint(std::string msg, int val) {
std::unique_lock<std::mutex> guard(m_mutex);
// do something 1
guard.unlock(); // 临时解锁
// do something 2
guard.lock(); //继续上锁
// do something 3
f << msg << val << std::endl;
std::cout << msg << val << std::endl;
// 结束时析构guard会临时解锁
// 这句话可要可不要,不写,析构的时候也会自动执行
// guard.ulock();
}
};
上面的代码可以看到,在无需加锁的操作时,可以先临时释放锁,然后需要继续保护的时候,可以继续上锁,这样就无需重复的实例化lock_guard
对象,还能减少锁的区域。同样,可以使用参数std::defer_lock
设置初始化的时候不进行默认的上锁操作:
void safePrint(std::string msg, int val) {
std::unique_lock<std::mutex> guard(m_mutex, std::defer_lock);
//do something 1
guard.lock();
// do something protected
guard.unlock(); // 临时解锁
// do something 2
guard.lock(); //继续上锁
// do something 3
f << msg << val << std::endl;
std::cout << msg << val << std::endl;
// 结束时析构guard会临时解锁
}
这样使用起来就比lock_guard
更加灵活!然后这也是有代价的,因为它内部需要维护锁的状态,所以效率要比lock_guard
低一点,在lock_guard
能解决问题的时候,就是用lock_guard
,反之,使用unique_lock
。
另外,请注意,unique_lock
和lock_guard
都不能复制,lock_guard
不能移动,但是unique_lock
可以!
// unique_lock 可以移动,不能复制
std::unique_lock<std::mutex> guard1(g_mutex);
std::unique_lock<std::mutex> guard2 = guard1; // error
std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok
// lock_guard 不能移动,不能复制
std::lock_guard<std::mutex> guard1(g_mutex);
std::lock_guard<std::mutex> guard2 = guard1; // error
std::lock_guard<std::mutex> guard2 = std::move(guard1); // error
参考:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!