RAII是C++的发明者Bjarne Stroustrup提出的概念,RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。即使用局部资源来管理对象,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
为什么需要用RAII来管理:
除去忘了写解锁语句这种低级错误,还可能加锁之后抛出异常,无法正常解锁
在RAII的指导下,我们应该使用(XMutex类)来管理资源,将资源(mux)和对象lock的生命周期绑定:
在要使用RAII机制的线程中定义一个XMutex类对象lock,XMutex类接受一个mutex类型的参数,其中有一个指向mutex的引用mux_,在lock执行其构造函数时通过对mux_加锁来对传进来的mux加锁,在该线程退出时lock执行其析构函数,通过对mux_解锁来对mux解锁,从而实现使用类来管理资源,将资源和对象的生命周期绑定。
using namespace std;
// RAII
class XMutex
{
public:
XMutex(mutex& mux):mux_(mux)
{
cout << "Lock" << endl;
mux_.lock();
}
~XMutex()
{
cout << "Unlock" << endl;
mux_.unlock();
}
private:
mutex& mux_;
};
static mutex mux;
void TestMutex(int status)
{
XMutex lock(mux);
if (status == 1)
{
cout << "=1" << endl;
return;
}
else
{
cout << "!=1" << endl;
return;
}
}
int main(int argc, char* argv[])
{
TestMutex(1);
TestMutex(2);
getchar();
return 0;
}
类成员是引用的话要在构造函数中使用初始化列表
在开始执行构造函数的函数体之前,要完成初始化。初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中,这是因为如果没有在构造函数初始值列表中显示地初始化成员,则该成员将在构造函数体之前执行默认初始化。之后,再进入构造函数体{}中。对于引用与const类型的成员,他们是很专一的,如果进行了默认初始化,他们指向的内容将不能改变,因此需要使用初始化列表。
由多种条件退出的话还要在每个退出前加上一个解锁,通过RAII来管理的话可以放心的退出,不用去操心解锁了。
可以看到,在测试线程中即使没有手动地解锁,也能调用unlock:
重要小结!!:
在C++多线程学习02 线程的入口参数中我们也看到了类似的利用对象自动销毁来控制生命周期的策略,所不同的是在02中我们是通过将对象的成员函数与this指针传给线程构造函数来将整个线程与对象生命周期绑定,这里我们是通过将互斥量mux做为RAII管理者类的构造函数的参数来将mux与对象生命周期绑定
C++11 实现严格基于作用域的互斥体所有权包装器lock_guard
std:lock_ guard类采用RAII手法管理某个锁对象,启动时在对象构造时将mutex加锁,无需手动调用lock方法,析构时对mutex解锁,这样保证在异常的情况下mutex可以在lock guard对象析构时被解锁,不会阻塞其它线程获mutex.
先来看看包装器lock_guard的定义:
template <class _Mutex>
class lock_guard {
// class with destructor that unlocks a mutex
//用解除互斥锁的析构函数类
public:
using mutex_type = _Mutex;
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t) : _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;
};
定义分析:
01lock_guard是一个模板类,可以接受各种类型的互斥量,如一般的互斥量,超时互斥量,共享互斥量等
02
explicit类型的构造函数和普通类型的构造函数的区别
普通的构造函数可以被显式调用和隐试调用,但是explicit的构造函数只能被显式地调用,不能被隐式地调用
lock_guard lock1(gmutex);显式调用
lock_guard lock2=lock1;隐式调用
lock_guard的第一个构造函数接收一个参数,只能显式调用
lock_guard的第二个构造函数接收两个参数,第二个参数adopt_lock的意思的收养锁(接管之前上好的锁),然后由lock对象来完成对lock的抚养工作(解锁),如果该代码之前没有上锁会报错:
03其拷贝构造以及重载的=被删除了,即不能使用lock_guard lock2=lock1或者lock_guard lock2(lock1),即
using namespace std;
// RAII
static mutex gmutex;
void TestLockGuard(int i)
{
gmutex.lock();
{
//已经拥有锁,不lock
lock_guard<mutex> lock(gmutex,adopt_lock);
//结束释放锁
}
{
lock_guard<mutex> lock(gmutex);
cout << "begin thread " << i << endl;
}
for (;;)
{
{
lock_guard<mutex> lock(gmutex);
cout << "In " << i << endl;
}
this_thread::sleep_for(500ms);
}
}
int main(int argc, char* argv[])
{
for (int i = 0; i < 3; i++)
{
thread th(TestLockGuard, i + 1);
th.detach();
}
getchar();
return 0;
}
临界区不一定包括整个函数,因为在锁住的临界区不应该有sleep,sleep的时候锁还在,其他线程进不来,因此使用{}来控制锁的临界区:
for (;;)
{
{
lock_guard<mutex> lock(gmutex);
cout << "In " << i << endl;
}
this_thread::sleep_for(500ms);
}
在进到{}中时通过lock_guard类对象的构造来对gmutex加锁,在退出{}时通过lock_guard类对象的析构来对gmutex解锁
可以看到123号线程依次加锁,cout,解锁