智能指针std::shared_ptr的循环引用问题

循环引用

    • 实质
    • 简介
    • 经典案例
    • 升阶案例
      • 函数简介
      • 问题描述
      • 解决方案

实质

外层对象与内层对象聚合,外层对象自然引用内层对象,当内层对象也引用外层对象,在销毁外层对象之前,内层对象需先解除对外层对象的引用,若销毁内层对象之时,解除引用的时机才到,则需先销毁外层对象。似此对象之间跨层级相互引用,而无法打破引用关系,导致循环引用。

简介

智能指针std::shared_ptr具有元素指针和引用计数基类指针,引用计数基类指针用于指向引用计数对象,引用计数对象继承自引用计数基类,并且具有元素指针或者元素空间。

引用计数基类具有两个计数器,分别是引用计数器和弱引用计数器。引用计数器控制元素的销毁或者析构时机,弱引用计数器控制引用计数对象的销毁时机。

经典案例

双向链表的简化代码:

#include 
#include 

struct Node
{
	std::shared_ptr<Node> prev;
	std::shared_ptr<Node> next;
	~Node() { std::cout << "~Node" << std::endl; }
};

int main()
{
	auto head = std::make_shared<Node>();
	auto tail = std::make_shared<Node>();
	head->next = tail;
	tail->prev = head;
	return 0;
}

即将退出函数之时,其内局部变量因超出作用域而结束生命周期,因此依次销毁智能指针std::shared_ptr对象tail和head。

在析构tail之时,head所指节点的后继仍指向tail所指节点及其引用计数对象,造成引用计数对象的两个计数器都未归零,即tail所指节点及其引用计数对象无法销毁。

在析构head之时,由于tail所指节点未被销毁,而节点的前驱仍指向head所指节点及其引用计数对象,同样造成引用计数对象的两个计数器未归零,即head所指节点及其引用计数对象无法销毁。

最终,所有节点及其引用计数对象都未正常销毁,导致资源泄漏。

升阶案例

线程池eterfree::ThreadPool的简化代码:

#include 
#include 
#include 

class ThreadPool
{
	struct ThreadPoolStructure
	{
		std::function<void(bool)> callback;
		~ThreadPoolStructure() { std::cout << "~ThreadPoolStructure" << std::endl; }
	};
	std::shared_ptr<ThreadPoolStructure> data;
public:
	ThreadPool()
		: data(std::make_shared<ThreadPoolStructure>())
	{
		data->callback = [data = data](bool) {};
	}
};

int main()
{
	ThreadPool threadPool;
	return 0;
}

函数简介

函数std::make_shared先创建引用计数对象,由引用计数对象构造元素,再创建智能指针std::shared_ptr的临时对象,指向元素和引用计数对象,之后返回临时对象。

问题描述

构造ThreadPool对象之时,先在构造函数体之外,调用std::make_shared函数创建指向ThreadPoolStructure变量的std::shared_ptr对象,用于复制构造成员变量data。于函数体内运用Lambda表达式初始化ThreadPoolStructure变量的函数子callback,在Lambda表达式之中,以成员变量data复制构造非局部变量data,二者一同指向ThreadPoolStructure变量及其引用计数对象。

析构ThreadPool对象之时,调用成员变量data的析构函数,由于存在非局部变量data,引用计数对象的两个计数器都不归零,即ThreadPoolStructure变量及引用计数对象未被销毁,而ThreadPoolStructure变量的函数子callback未被析构,因此非局部变量data无析构时机,最终导致资源泄漏。

解决方案

于案例代码寻找以下代码:

data->callback = [data = data](bool) {}

将其改为下述代码:

data->callback = [data = std::weak_ptr(data)](bool) {}

智能指针std::shared_ptr同时访问引用计数器和弱引用计数器,而智能指针std::weak_ptr只访问弱引用计数器。

非局部变量data由std::shared_ptr对象变为std::weak_ptr对象。当析构ThreadPool成员变量data时,即使存在非局部变量data,引用计数对象的引用计数器也将归零,于是析构ThreadPoolStructure变量及其函数子callback,从而析构非局部变量data,使弱引用计数器归零,销毁引用计数对象。

你可能感兴趣的:(C/C++,线程池)