常见问题
在一个函数中(或者一个{...}
作用域)有时候会创建/引用了一个资源,而在这个函数结束的时候需要对这个资源进行释放。常见的场景:
- 申请了一段内存,退出时候需要释放
- 打开了一个文件,退出需要关闭文件
- 统计某个函数调用的当前引用次数,进入的时候引用加一,退出的时候引用减一
- 某个
{...}
作用域开始需要加锁,执行完代码后需要解锁 - 等等…
以上面的锁为例, 在进入函数的时候加锁,在函数退出的时候解锁。
这种写法笔者认为可能会带来两个问题:
- 在互斥区的代码有可能会有多处返回
return
, 在每个return处都加上mutex.unlock()代码感觉显得很不优雅。 - 互斥区代码也有可能抛出异常,而有些场景,你并不想在互斥区捕获异常,那么也就不会调用mutext.unlock()从而导致锁并没有释放。
void function()
{
mutex.lock();
//互斥区执行代码;
//...
if(...)
{
//...
mutex.unlock();
return;
}
//...
if(...)
{
//...
mutex.unlock();
return;
}
//...
mutex.unlock();
}
而RAII
正是可以用来解决以上两个问题的,接下来我们来说说RAII
。
RAII以及lock_guard的实现
笔者是一个朴实的人,发现很多高大上的名字背后,都是些朴实无华的东西。RAII
全称为resource acquisition is initialization
, 可能现在还无法理解这个含义,等整篇文章读完后再理解下。
RAII主要利用了如下的机制:
- 一个对象在其变量作用域结束的时候会调用析构函数
- 实现方式: 将资源的获取放在一个对象的构造函数中,然后资源的释放放在这个对象的析构函数。
lock_guard
是C++11支持的,不过在此之前boost
很早实现,并被广泛使用。然后我们再以第一节的例子,使用lock_guard
来实现:
void function()
{
std::lock_guard lockGuard(mutex);
//互斥区执行代码;
//...
if(...)
{
//...
return;
}
//...
if(...)
{
//...
return;
}
//...
}
可以看到这个例子,我们做了两件事情,让代码简洁了很多:
- mutex作为
lockGuard
的构造函数参数传递进去 - 删除了
function
函数中所有的mutex.unlock();
那么小伙伴们结合之前的概念可能已经想到了lock_guard
的实现。以下是MSVC对lock_guard
的实现。那么可以使得:
- 在调用
lockGuard
的构造函数的时候,lockGuard
的成员_Mtx
对mutex
进行了引用;并且将调用了_MyMutex.lock();
。一句话描述:对象构造函数调用完毕后,则加锁了。此时是不是有点理解resource acquisition is initialization
。 lockGuard
的生命周期就在函数调用结束后return
或者函数内抛出异常,则locGuard
的析构函数会调用_MyMutex.unlock();
从而实现了锁的释放。
// CLASS TEMPLATE 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
{ // unlock
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
总结
RAII是C++常用的技术,那么我们有必要去理解他,并且利用他:
- 使用RAII可以有效的防止资源不及时释放引发的问题: 比如资源泄露,死锁等
- RAII的是一种思想,可以拓展到代码的很多场景: 比如从资源池拿到的资源,使用后放回资源池。
- RAII中提到的对象的作用域,提醒下一些新手朋友,不一定是指函数。比如你可以在函数内部使用
{...}
来指定你的作用域(如下所示),灵活的锁定范围。
void function()
{
//some code
//.......
{
RAIIObject robj(object);
//Do something
}
//some code
//......
}
最后是个人微信公众号,文章CSDN和微信公众号都会发,欢迎一起讨论。