写点什么

谈谈 enabled_shared_from_this

作者:SkyFire
  • 2023-01-29
    陕西
  • 本文字数:2911 字

    阅读完需:约 10 分钟

应用场景

先看一段代码:


#include <iostream>#include <thread>#include <chrono>#include <future>#include <string>
using namespace std;
struct Obj;
void ayncCallback(Obj* obj);
struct Obj{ string data = "hello world"; future<void> putMeToAsyncCallback() { return async(ayncCallback, this); // 将this传递给一个异步函数 } ~Obj() { cout << "~Obj" << endl; }};
void ayncCallback(Obj* obj){ // 模拟一些耗时长的操作 this_thread::sleep_for(1s); // 访问obj对象 cout << "print from aync task: " << obj->data << endl;}
int main(){ auto pobj = new Obj; auto f = pobj->putMeToAsyncCallback(); delete pobj; f.wait(); return 0;}
复制代码


如上述的代码描述,在 Obj 类中,需要将对象指针传出,比如如示例描述的传递给一个异步操作。


这里遇到的问题就是此对象的声明周期难以管理,比如例子中,pobj 被销毁了,但是异步操作还在访问 pobj 指向的对象。


想要解决此种问题的方法一般有两个:


  1. 明确对象的声明周期,线程间增加同步措施。

  2. 显然这种方案会显著增加线程同步的复杂性,需要开发人员对每个异步对象的声明周期一清二楚。在稍微复杂的系统中,这几乎是不可能的事。

  3. 使用 shared_ptr 自动管理内存。


对于第二种方案,我们来看看如何实现。


首先,修改ayncCallback函数的签名:


void ayncCallback(shared_ptr<Obj> obj);
复制代码


注意到 shared_ptr 的构造函数可以直接接受裸指针,尝试在传参的时候构造一个智能指针:


future<void> putMeToAsyncCallback(){    return async(ayncCallback, shared_ptr<Obj>(this)); // 将this传递给一个异步函数}
复制代码


同时将 main 中的 delete 删除,因为此时 obj 对象已经由智能指针管理了。


当前阶段完整的代码如下:


#include <iostream>#include <thread>#include <chrono>#include <future>#include <string>
using namespace std;
struct Obj;
void ayncCallback(shared_ptr<Obj> obj);
struct Obj{ string data = "hello world"; future<void> putMeToAsyncCallback() { return async(ayncCallback, shared_ptr<Obj>(this)); // 将this传递给一个异步函数 } ~Obj() { cout << "~Obj" << endl; }};
void ayncCallback(shared_ptr<Obj> obj){ // 模拟一些耗时长的操作 this_thread::sleep_for(1s); // 访问obj对象 cout << "print from aync task: " << obj->data << endl;}
int main(){ auto pobj = new Obj; auto f = pobj->putMeToAsyncCallback(); f.wait(); return 0;}
复制代码


测试可以正常运行。


但是如果我们调用两次 putMeToAsyncCallback 会怎么样呢?


新的 main 函数如下:


int main(){    auto pobj = new Obj;    auto f1   = pobj->putMeToAsyncCallback();    auto f2   = pobj->putMeToAsyncCallback();    f1.wait();    f2.wait();    return 0;}
复制代码


编译运行,再次发生内存访问异常,这是什么原因呢?


因为 putMeToAsyncCallback 中每次都会创建一个智能指针来管理同一个底层的裸指针,而这些对象各自拥有自己的引用计数。因此在每个 ayncCallback 函数结束的时候,对象会被释放一次,因此在下次对象被使用的时候已经是已经被释放的对象了。


智能指针的结构如下图所示,红色虚线表示引用计数指针,绿色实线表示对象指针。



因此,从 this 产生 shared_ptr 不能直接使用 shared_ptr 的构造函数。接下来看一下正确的做法。


先看一下代码:


#include <iostream>#include <memory>#include <thread>#include <chrono>#include <future>#include <string>
using namespace std;
struct Obj;
void ayncCallback(shared_ptr<Obj> obj);
struct Obj : public enable_shared_from_this<Obj>{ string data = "hello world"; future<void> putMeToAsyncCallback() { return async(ayncCallback, shared_from_this()); // 通过shared_from_this从this指针产生智能指针 } ~Obj() { cout << "~Obj" << endl; }};
void ayncCallback(shared_ptr<Obj> obj){ // 模拟一些耗时长的操作 this_thread::sleep_for(1s); // 访问obj对象 cout << "print from aync task: " << obj->data << endl;}
int main(){ auto pobj = make_shared<Obj>(); auto f1 = pobj->putMeToAsyncCallback(); auto f2 = pobj->putMeToAsyncCallback(); f1.wait(); f2.wait(); return 0;}
复制代码


此代码相比之前的代码的修改点主要有以下:


  1. 使 Obj 类继承自 enabled_shared_from_this<Obj>,使其具备 shared_from_this 方法。

  2. 将自身指针传递给异步函数调用的时候,不再使用 shared_ptr 的构造函数构造一个智能指针,而是使用 shared_from_this 函数产生一个智能指针。

  3. main 函数中创建对象不再使用 new,而是使用 make_shared

实现原理

前面无法正常工作的代码中,出现内存访问异常的原因是同一个对象由多个智能指针共同管理,但是这些智能指针间没有任何关系,如果可以在这些智能指针间建立关系,使他们共享底层的引用计数,那么这个问题就可解决了。


事实上,enabled_shared_from_this 模板类也确实是这样做的。


Obj 继承自 enabled_shared_from_this<Obj> ,会在基类中引入一个 weke_ptr<Obj>对象,这个对象是 Obj 对象的成员,在调用 shared_from_this 的时候,会从 weak_ptr 生成一个 shared_ptr,这些 shared_ptr 共享引用计数。


为什么保存的时候 weke_ptr 而不是 shared_ptr 呢?因为这个智能指针保存在对象本身的成员中,并指向本身,如果是 shared_ptr,那么会有循环引用的问题。


在智能指针的构造过程中,会检测类型是否派生自 enabled_shared_from_this<T> 类,如果是,则会为对象内部的 weak_ptr 赋值。后续在对象上调用 shared_from_this 的时候,就直接有可用的 weak_ptr 来生成 shared_ptr 了。


智能指针的结构如下所示,对象所有的 shared_ptr 和 weak_ptr 共享相同的引用计数和底层对象。


注意事项

从上面的分析可以看出,对象内部的 weak_ptr 初始化依赖于智能指针的创建,也就是说,对象必须被智能指针管理才能调用 shared_from_this。举例如下,将 main 函数修改为:


int main(){    auto pobj = new Obj;    auto f1   = pobj->putMeToAsyncCallback();    auto f2   = pobj->putMeToAsyncCallback();    f1.wait();    f2.wait();    return 0;}
复制代码


编译运行会报错:


terminate called after throwing an instance of 'std::bad_weak_ptr'

what(): bad_weak_ptr


说明对象中的 weak_ptr 未被初始化。

总结

  1. enabled_shared_from_this 主要用于对象需要将自身的指针传递给其他流程,但是声明周期不好管理,需要依赖智能指针管理的场景。

  2. enabled_shared_from_this 的原理是在对象中增加一个 weak_ptr,从而使所有从 shared_from_this 函数创建出来的智能指针共享底层引用计数。

  3. 对象中的 weak_ptr 对象是在创建智能指针对象的时候初始化的,因此如果对象没有被智能指针管理,则 shared_from_this 会抛出异常。

发布于: 刚刚阅读数: 5
用户头像

SkyFire

关注

这个cpper很懒,什么都没留下 2018-10-13 加入

会一点点cpp的苦逼码农

评论

发布
暂无评论
谈谈enabled_shared_from_this_c++_SkyFire_InfoQ写作社区