1. std::lock_guard
std::lock_guard
是 C++ 标准库中的一个类模板,位于 <mutex>
头文件中。它提供了一种便利的 RAII(资源获取即初始化)机制,确保在进入作用域时锁定互斥锁,在退出作用域时解锁互斥锁,从而避免了手动管理互斥锁的复杂性和可能的错误。
其作用主要有两个方面:
-
自动加锁和解锁:
std::lock_guard
在构造时会锁定给定的互斥锁,并在析构时释放互斥锁。这意味着,无论通过正常执行还是通过异常退出,互斥锁都会被正确地释放。 -
防止死锁:
std::lock_guard
的设计有助于避免由于忘记释放互斥锁而导致的死锁问题。因为std::lock_guard
是根据作用域来管理锁的生命周期的,它确保在作用域结束时释放锁,因此减少了手动管理锁的需求,从而减少了出错的可能性。
2. 源码剖析
_EXPORT_STD template <class _Mutex>
class _NODISCARD_LOCK lock_guard { // class with destructor that unlocks a mutex
public:
// 使用 mutex_type 作为互斥锁类型别名
using mutex_type = _Mutex;
// 构造函数,接受一个互斥锁的引用作为参数,并在构造时锁定该互斥锁
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
_MyMutex.lock();
}
// 构造函数,接受一个互斥锁的引用和一个`adopt_lock_t`类型的参数。这个构造函数不会锁定互斥锁,而是假设调用者已锁定互斥锁
lock_guard(_Mutex& _Mtx, adopt_lock_t) noexcept // strengthened
: _MyMutex(_Mtx) {} // construct but don't lock
// 析构函数,会在对象被销毁时解锁互斥锁
~lock_guard() noexcept {
_MyMutex.unlock();
}
// 禁用了拷贝构造函数和赋值运算符,以确保不能通过拷贝或赋值的方式进行对象的复制或赋值,
// 这是因为拷贝或赋值的话会产生不可预测的行为,可能导致重复解锁已经被解锁的互斥锁。
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
此外,这段代码使用了noexcept
关键字来声明不会抛出异常的函数,这是一种优化的技术,有助于编译器生成更加高效的代码。
3.示例
#include <iostream>
#include <thread>
#include <mutex>
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';
}
在 safe_increment()
函数中,std::lock_guard<std::mutex>
对象 lock
在函数体内被创建,它会自动锁定 g_i_mutex
。当函数执行完成时,lock
对象离开作用域,会自动解锁 g_i_mutex
。这样就确保了 ++g_i
这个临界区内的代码只会被一个线程执行,从而避免了竞态条件。