[cpp]资源生命周期管理类:enable_shared_from_this

enable_shared_from_this模板类能够帮助我们轻松的用对象在其方法中获取指向对象的shared_ptr, 从而在并行编程中安全的管理资源的生命周期, 避免跨线程调用中资源的提前释放导致程序出错的危害.

我们在之前简单学习了基于RAII的资源管理类的原理和实现, 并实现了一个简易的shared_ptr类来安全的管理资源. shared_ptr模板类使用共享的Control Block来管理资源的生命周期, Control Block记录了引用计数、weak引用计数和其他必要的信息来管理资源. 当使用raw指针构造一个shared_ptr对象时, 新的Control Block就会被创建. 因此为了保证只有唯一一个Control Block来管理资源, 必须用已有的shared_ptr对象使用拷贝构造\拷贝赋值的方式创建新的shared_ptr对象. 从而避免资源被多次释放造成的程序出错.

下面给出一个典型的错误使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <memory>

using namespace std;

int main() {
{
int* p = new int(1234);
shared_ptr<int> sp1 {p};
shared_ptr<int> sp2 {p};
cout << "ref Count = " << sp1.use_count() << endl;
}
cout << "Done\n";
return 0;
}

/* Output on Linux Ubuntu g++ 9.4.0
ref Count = 1
free(): double free detected in tcache 2
已放弃 (核心已转储)
*/

因此我们必须避免同一个资源被多个shared_ptr对象管理, 这会造成程序出错.

对象的生命周期

在并行(异步)编程中, 一个线程可能会调用其他线程的函数异步的完成某些任务, 而该任务依赖于当前线程所用的对象资源, 这种情况下, 必须保证该对象资源的生命周期必须比异步函数的生命周期要长, 因为如果在异步函数执行的过程中它所用的对象被其他线程析构了, 那么会造成程序崩溃. 因此我们必须使用一些方法保证对象资源的生命周期, 一般实践中使用对象的this指针来传递对象的上下文, 保证该对象的跨线程生命周期. 简单来说, 我们使用this指针保证A线程中的对象资源在调用B线程中的异步方法, 且该方法使用该对象资源时, 该对象资源的生命周期必须长于B线程中的异步函数, 即其在B线程中异步函数生命周期中不可被其他线程释放.

为了实现上述需求, 我们可以使用shared_ptr来管理this指针(管理对象资源). 那么该如何管理呢?
我们考虑从该对象本身构造出指向其自身的shared_ptr对象. 我们需要从shared_ptr所管理的对象中获取其指向自身的shared_ptr对象时, 如果我们简单的使用类的成员函数返回指向自身的shared_ptr对象, 那么根据以上分析, 这将会导致程序出错.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <memory>

using namespace std;

class Resource {
public:
Resource (int res) : _res(res) {}
~Resource () {
cout << "called dest." << endl;
}
shared_ptr<Resource> getObject() {
return shared_ptr<Resource>(this);
}

private:
int _res;
};

int main() {
{
shared_ptr<Resource> sp = make_shared<Resource>(10);
auto objSp = sp -> getObject();
}
cout << "Done\n";
return 0;
}

/* Output on Linux Ubuntu g++ 9.4.0
called dest.
double free or corruption (out)
已放弃 (核心已转储)
*/

上面错误使用场景和最开始提到的场景是一模一样的,即都用了多个Control Block来管理指针资源, 从而导致重复释放. 这种情况下, 我们就需要enable_shared_from_this来实现上述需求.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <memory>

using namespace std;

class Resource : public enable_shared_from_this<Resource> {
public:
Resource(int res) :
_res(res) {
}
~Resource() {
cout << "called dest." << endl;
}
shared_ptr<Resource> getObject() {
return shared_from_this();
}

private:
int _res;
};

int main() {
{
shared_ptr<Resource> sp = make_shared<Resource>(10);
cout << "ref cnt = " << sp.use_count() << endl;
auto objSp = sp->getObject();
cout << "ref cnt = " << sp.use_count() << endl;
}
cout << "Done\n";
return 0;
}

/* Output on Linux Ubuntu g++ 9.4.0
ref cnt = 1
ref cnt = 2
called dest.
Done
*/

需要实现上述需求的资源类需要公有继承enable_shared_from_this模板类, 然后使用shared_from_this方法(enable_shared_from_this模板类的公有方法)来获取一个指向其对象自身shared_ptr对象.

我们简单来看以下enable_shared_from_this模板类的源码, 看看其是如何实现的上述功能的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* @brief Base class allowing use of member function shared_from_this.
*/
template <typename _Tp>
class enable_shared_from_this {
protected:
constexpr enable_shared_from_this() noexcept {
}

enable_shared_from_this(const enable_shared_from_this &) noexcept {
}

enable_shared_from_this &
operator=(const enable_shared_from_this &) noexcept {
return *this;
}

~enable_shared_from_this() {
}

public:
/* 公有方法 */
shared_ptr<_Tp>
shared_from_this() {
return shared_ptr<_Tp>(this->_M_weak_this);
}

shared_ptr<const _Tp>
shared_from_this() const {
return shared_ptr<const _Tp>(this->_M_weak_this);
}

#if __cplusplus > 201402L || !defined(__STRICT_ANSI__) // c++1z or gnu++11
#define __cpp_lib_enable_shared_from_this 201603
weak_ptr<_Tp>
weak_from_this() noexcept {
return this->_M_weak_this;
}

weak_ptr<const _Tp>
weak_from_this() const noexcept {
return this->_M_weak_this;
}
#endif

private:
template <typename _Tp1>
void
_M_weak_assign(_Tp1 *__p, const __shared_count<> &__n) const noexcept {
_M_weak_this._M_assign(__p, __n);
}

// Found by ADL when this is an associated class.
friend const enable_shared_from_this *
__enable_shared_from_this_base(const __shared_count<> &,
const enable_shared_from_this *__p) {
return __p;
}

template <typename, _Lock_policy>
friend class __shared_ptr;

mutable weak_ptr<_Tp> _M_weak_this;
};

以上源码我们发现三点:

  • enable_shared_from存在一个mutable成员变量weak_ptr<_Tp> _M_weak_this, 这样const对象也能够对其进行修改.
  • enable_shared_from含有友元类__shared_ptr, 这样shared_ptr类能够访问enable_shared_from的私有成员变量.
  • cpp17添加了weak_from_this()方法返回weak_ptr的拷贝, 然后使用weak_ptr.lock()就能安全的获取shared_ptr对象.

具体shared_from_this()函数的实现是很简单的, 其通过私有成员变量来构造shared_ptr对象然后返回. 那么该私有成员变量是何时初始化的呢?

它是在构造shared_ptr对象的时候被初始化的, 在初始化构造一个shared_ptr对象的时候, 可以根据type traits(std::enable_ifstd::is_convertible)来实现. 如果这个资源类公有继承std::enable_shared_from_this模板类, 那么就将父类中的_M_weak_this初始化绑定到创建出来的shared_ptr对象上去(友元类的声明让其能够访问私有成员变量), 这样就实现了_M_weak_this的安全初始化. 这种设计对于shared_ptr模板类来说是侵入式的.

最后给出一个讲解很好博客的图示和代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Article : std::enable_shared_from_this<Article> {
//stuff..
};

void foo() {
//Step 1
// '_M_weak_this' 是空的, 没有和 Control Block 相关联
auto pa = new Article;

//Step 2
// '_M_weak_this' 被初始化, 与 Control Block 相关联
auto spa = std::shared_ptr<Article>(pa);
}

图示

需要注意的是, 公有继承的原因是_M_weak_this的初始化是在shared_ptr对象的构造函数中初始化的, 必须能够检测到该类是继承了enable_shared_from_this基类的. 另外, 必须通过shared_ptr来调用对象的shared_from_this, 因为_M_weak_this的初始化是在shared_ptr对象的构造函数中进行的, 如果还没有shared_ptr对象被构造, 那么调用shared_from_this()使用_M_weak_this来构造shared_ptr会造成std::bad_weak_ptr异常, 原因是_M_weak_this还没有和某个Contorl Block相关联. 当然如果使用cpp17, 可以用weak_from_this()来获取weak_ptr自行lock()并判断来保证安全, 不过还是建议统一先构造shared_ptr对象, 再安全的使用shared_from_this方法.

总结

当一个类需要”共享自己”的时候, enable_shared_from_this模板类就是标准库提供的强大工具. 它可以安全的管理和构造我们所需的shared_ptr对象, 不过还是有一些问题是需要注意的. 首先我们必须先构造shared_ptr对象, 然后使用类的shared_from_this方法, 因为必须保证enable_shared_from_this基类的_M_weak_this被初始化. 其次我们不能在类的构造函数中调用shared_from_this, 因为此时_M_weak_this可能还未初始化. 而且必须公有继承enable_shared_from_this.

最后对象的生命周期管理是多线程(异步)编程中必须关注的一个问题, cpp提供的智能指针和enable_shared_from_this等模板类很友好的实现了我们的需求, 不过仍然需要注意一些坑点. 向往侯捷老师说的胸中自有丘壑的境界水平, 努力做到知其然知其所以然, 才能提升自身的视野和水平呀.

参考

cppRef
图文讲解blog

作者

Jsss

发布于

2022-04-26

更新于

2022-05-29

许可协议


评论