没有学不会的C++:RAII 技术(Resource Aquisition is Initialization)

RAII(Resource Aquisition is Initialization)技术是用对象来管理资源的一种技术,资源可以指内存、socket、IPC 等。

用 RAII 管理锁资源

这个概念比较抽象,我们还是从具体的例子中学习,一般我们这样使用互斥锁:

pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
void functionA() {
    pthread_mutex_lock(&mu);
    // ... 操作共享资源
    pthread_mutex_unlock(&mu); 
}

即,我们在使用共享资源之前通过 pthread_mutex_lock 加锁,并在使用完资源后,通过 pthread_mutex_unlock 解锁,但这种代码隐患极大,因为你不能保证锁一定会释放,例如在使用资源的时候可能抛出异常,那么这个锁就永远得不到释放,那有什么办法可以让锁一定释放,甚至自动释放呢?那就要用到今天提到的 RAII 技术:我们用对象来管理锁,对象存储在栈中,利用代码块在退出时会自动释放栈资源的特性,锁也会自动得到释放,如下面的代码:

#include <pthread.h>

pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
class Lock {
private:
    pthread_mutex_t* m_pm;
public:
    explicit Lock(pthread_mutex_t* pm) { pthread_mutex_lock(pm); m_pm = pm; }
    ~Lock() { pthread_mutex_unlock(m_pm); }
};

void functionA() {
    Lock mylock(&mu);
    // ... 操作共享资源
    // mutex会在函数退出时自动释放
}

上面代码中,Lock 构造函数接受一个 mutex 指针,同时会调用 pthread_mutex_lock 加锁,并会在该对象被析构时,调用 pthread_mutex_unlock 解锁,这就做到了对象创建时加锁,释放时解锁的效果,如果我们把这个对象放到栈中,则锁资源也会随着该对象在栈中的生命周期进行自动的加锁和解锁,函数或者代码块都可以构造这样的上下文。而这种用对象来管理资源的方式,就是我们开篇所说的 RAII。

shared_ptr 也是一种 RAII

另一个典型的使用 RAII 技术的例子是 std::shared_ptr,我们通过 shared_ptr 来管理资源——一般是堆中申请的对象,shared_ptr 通过引用计数来管理指针对象,我们对 shared_ptr 进行复制,引用计数就加 1,相反,如果减少一个 shared_ptr,引用计数就减 1,当引用计数减到 0 时,会自动调用 delete 释放指针对象,下面的代码使用了一个 pd 智能指针来管理 dog 对象,当 pd 退出作用域,如果没有额外的智能指针引用 dog,则 dog 会被自动释放:

int function_A() {
    // pd 退出作用域时,dog 会自动释放
    std::shared_ptr<dog> pd(new dog());
}

下面我们来看一下使用 shared_ptr 的一个陷阱,代码如下:

class dog;
class Trick;
void train(std::shared_ptr<dog> pd, Trick dogtrick);
Trick getTrick();

int main() {
    train(std::shared_ptr<dog> pd(new dog()), getTrick());
}

函数 train 是一个训练函数,它接受两个参数:dogTrick,即具体训练 dog 的方法由 Trick 提供,但实际上这行代码是有问题的,问题在于,编译器调用 new dog()getTrick()shared_ptr<dog> pd() 这三个函数的顺序是不确定的,如果编译器正好按照以下顺序来执行:

  1. new dog()
  2. getTrick()
  3. shared_ptr<dog> pd()

同时在执行到第 2 步 getTrick() 时抛出了异常,那么 dog 指针就没有被智能指针管理起来,于是就发生了内存泄漏。这个问题怎么解决,我们把 train 这行代码拆成两行就可以了,如下:

int main() {
    std::shared_ptr<dog> pd(new dog());
    train(pd, getTrick());
}

所以

在初始化 shared_ptr 时,不要和其他语句放在一起使用

RAII 对象的复制问题

最后,我们再来看一个 RAII 对象复制的问题,仍然是上文定义的锁 Lock,如果对 Lock 对象调用赋值构造函数,即:

Lock L1(&mu);
Lock L2(L1);

此时 m_pm 会被多个 RAII 对象持有,且因为每个 RAII 对象析构时都会对 m_pm 进行解锁,所以程序就无法控制该锁的解锁时机了,因此,为了解决这问题,我们首先想到的方案就是禁止 Lock 对象的复制能力,具体做法可以参考之前的文章《没有学不会的 C++:禁止成员函数(disallow functions)》

今天我们来学习另外一种解决方案,即使用智能指针 shared_ptr 来解决 RAII 锁的复制问题,思路是这样的,因为智能指针只有在引用计数减为 0 时,才执行真正的「清理」工作,如果把「清理」换成解锁,我们就不用担心多次解锁的问题。

正好,shared_ptr 支持用户自定义「清理」方法,如下是 shared_ptr 的声明

template<class Other, class D> shared_ptr(Other* ptr, D deleter);

第二个参数是引用计数为 0 时调用的「清理」函数,默认会使用 delete,所以在锁场景,我们把它替换为 pthread_mutex_unlock 即可,完整的代码如下:

pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
class Lock {
    private:
        std::shared_ptr<pthread_mutex_t> pMutex;
    public:
        explicit Lock(pthread_mutex_t *pm)
            : pMutex(pm, pthread_mutex_unlock) {
                pthread_mutex_lock(pm);
            }
};

可以看到,LockpMutex 是一个 pthread_mutex_t 类型的智能指针,它在构造函数被调用时初始化,且 deleterpthread_mutex_unlock,同时会调用 pthread_mutex_lock 进行加锁,这种机制不限制 Lock 的复制,且只有在所有「复制品」都释放时,才自动调用 pthread_mutex_unlock 进行解锁,这是非常理想的使用 RAII 控制锁的方法。

参考:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容