应用场景
先看一段代码:
#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 指向的对象。
想要解决此种问题的方法一般有两个:
明确对象的声明周期,线程间增加同步措施。
显然这种方案会显著增加线程同步的复杂性,需要开发人员对每个异步对象的声明周期一清二楚。在稍微复杂的系统中,这几乎是不可能的事。
使用 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;
}
复制代码
此代码相比之前的代码的修改点主要有以下:
使 Obj 类继承自 enabled_shared_from_this<Obj>,使其具备 shared_from_this 方法。
将自身指针传递给异步函数调用的时候,不再使用 shared_ptr 的构造函数构造一个智能指针,而是使用 shared_from_this 函数产生一个智能指针。
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 未被初始化。
总结
enabled_shared_from_this 主要用于对象需要将自身的指针传递给其他流程,但是声明周期不好管理,需要依赖智能指针管理的场景。
enabled_shared_from_this 的原理是在对象中增加一个 weak_ptr,从而使所有从 shared_from_this 函数创建出来的智能指针共享底层引用计数。
对象中的 weak_ptr 对象是在创建智能指针对象的时候初始化的,因此如果对象没有被智能指针管理,则 shared_from_this 会抛出异常。
评论