今天在评审代码的时候,因为位于两个不同的线程中(一个是周期性事件线程,一个是触发式事件线程),需要对一个资源类的某些属性进行互斥的访问,因此采用
lock_guard
互斥量包装器,但是在升级的过程中,因为整个系统太大,所以在询问了某位同事后,得到的答案是在两个不同的地方加上lock_guard
有一定的可能性会导致死锁,但是后面在测试的过程中又没有问题,真的如此吗?本文针对lock_guard
来做阐述和延申
历史原因:为什么要使用lock_gurad,有什么优点
传统的C++
对互斥量加锁是用互斥量本身的锁的,即
#include <mutex>
std::mutex g_mutex;
// 加锁
g_mutex.lock();
// 解锁
g_mutex.unlock();
此时会有两个不便利处,即要手动的解锁,如果忘了解锁,那就完蛋了!,因此采用lock_guard
#include <mutex>
std::mutex g_mutex;
{
// 作用域开始
std::lock_guard<std::mutex> lock(g_mutex);
} // 作用域结束
lock_guard
以作用域为加解锁单位,退出当前作用域自动解锁,不必手动解锁。总结一下它的优点
RAII
(资源获取即初始化)语法:std::lock_guard
使用对象的构造函数和析构函数来自动管理互斥量的锁定和解锁,从而避免了手动管理锁的复杂性。这种自动化的资源管理方式确保在作用域结束时释放锁,防止忘记解锁而导致的资源泄漏或死锁- 异常安全:由于
std::lock_guard
使用RAII
语法,即使在作用域内发生异常,也会自动调用析构函数释放锁,确保了异常安全性。这样可以避免在异常发生时锁没有被释放而导致的资源泄漏或死锁 - 简单易用:
std::lock_guard
提供了一种简单而直观的方式来管理互斥量,不需要手动调用lock()
和unlock()
函数,因此代码更加简洁清晰,易于理解和维护 - 避免死锁:由于
std::lock_guard
在构造时立即锁定互斥量,在析构时立即释放互斥量,因此减少了出现死锁的可能性。通过使用std::lock_guard
,程序员可以更容易地确保正确的加锁和解锁顺序,从而避免死锁 - 线程安全性:
std::lock_guard
是线程安全的,可以在多线程环境下安全地使用。它能够确保在同一时间只有一个线程可以访问被互斥量保护的资源,从而保证了线程安全性
lock_guard和unique_lock的区别
延迟加锁:可以在构造时不立即锁定互斥量,在需要时手动调用 lock() 函数来锁定互斥量。这种延迟加锁的特性允许在一段代码中多次锁定和解锁互斥量,提供了更多的灵活性
void FunThread()
{
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁,此时不加锁
lock.lock(); // 加锁
//
lock.unlock(); // 解锁
}
条件等待:提供了与条件变量一起使用的功能,可以在条件变量的等待和通知过程中自动锁定和解锁互斥量。通过将 std::unique_lock
对象传递给条件变量的wait()
和 notify_*()
函数,可以轻松实现条件等待的功能
配合条件变量使用的时候必须使用unique_lock
,因此lock_guard
不会自动释放锁,试想,在wait
之前,本线程已经持有锁了,在等待的时候,如果一直持有互斥量,那其他线程也会拿不到互斥量,执行不了逻辑,则会导致永久死锁,可见源码
template <class _Lock, class _Predicate>
bool wait(_Lock& __lock,
stop_token __stoken,
_Predicate __p)
{
if (__stoken.