RCGC 算法 - 通过修正 shared_ptr 的循环引用问题从而避免weak_ptr的引入

[2020-07-30-03:53:58] 作者 杨逸林(现网名:杨铸人)

RCGC 算法

这几天因为项目的需要,又拿起了COM(Common Object Model),因为不能用ATL或者MFC, 只能自己亲手重写IUnknown。这就涉及到AddRef以及Release的具体实现方法,以及那个 m_RefCount的初始值等细节上的问题。

一番折腾之后,还是正确的实现了需要的COM类和对象。但是,既然是C++,C++显然有更好的 处理方式:也就是shared_ptr,这样一个模板类。

似乎不管啥指针,套在shared_ptr里面,C++都能把它自动管理好。然而,它也有问题, 也就是众所周知的循环引用问题。如果A类的对象里面通过shared_ptr引用了B类的对象,而B类 的这个对象又在其中引用了A类的那个对象,那么shared_ptr使用的引用计数机制,就不灵了。

为啥呢?这就涉及到了shared_ptr的实现问题。总得来说,shared_ptr,把每个指针都对应一个数量 记录下来,也就是引用计数。正常的情况下,引用计数总是和释放的次数相应。就是说,你引用了 一个对象多少次,你就释放多少次就行了。可是,如果涉及到循环引用,就会出问题:当引用到包含 了自己的对象的时候,引用计数就会比释放的次数多1次,两个对象互相引用,总共多2次。

这两次多余的引用次数,就导致在释放对象的时候不能计数不能清零,最终也不会达到释放对象占用 内存的条件。而如果用COM那种方式,恐怕情况会更糟:delete this 对于循环引用来说,很可能造成 对象析构函数之间的间接递归调用,最终导致栈溢出。

标准C++的处理方式,是引入weak_ptr,也是一种智能指针。只是这种指针,你要用它的时候,得首先问问, 它指向的对象是否真的存在。这当然也是有效的方法,只是说,啥时候用weak_ptr,啥时候用shared_ptr,自己得好好设计一下。

那么,有没有办法,就只用shared_ptr,也能避免循环引用的问题呢?

其实,循环引用,并不是真正的绊脚石。如果一个智能指针,确实能够记得住它底层指针到底被用了多少次, 那么真的就是引用多少次,释放多少次即可。但是实际设计上,我们总是倾向在计数为0的时候,直接释放底层指针 所对应的对象(调用析构函数)以及它所占据的内存(对其调用free)。而这两个步骤,正好就是delete依次 完成的。

可是delete先是调用了析构,然后又释放了内存,而内存中还有智能指针记录的数据。也就是说,在delete释放 内存的这一步,智能指针里面的数据也丢了,这就让它所占据的那个计数,没能被减去。或者如果一定要它被减去, 则被释放的内存,还得保持原来的数据。

在Debug模式中,原来的数据肯定是要清除的。虽然在Release模式中不清除,但这是危险的做法(一定不要依赖)。 所以,若要保证所有的智能指针都正确的工作,把每一个计数都正确的减去,我们就不能让这个指针所占据的内存 轻易的被释放:我们可以考虑把delete的两个阶段拆开来处理。

第一个阶段,是对要被delete的对象,调用析构函数。而析构函数自动调用内部的智能指针的析构函数。我们在 智能指针的析构函数被调用的时候,主动调用智能指针的底层对象的析构函数,却避免释放内存(不调用delete, 也不调用free)。这样的话,底层指针指向的对象中的智能指针的析构函数又会被调用,而结果则是其底层指针的 析构函数被调用,整个过程中没有任何内存被释放掉,系统自动调用每个底层对象的析构函数以及其含有的智能指针 的析构函数,在这些调用之中,只减少引用计数,不释放任何内存。如果发现任何底层对象的引用计数已经为0, 则把它放在一个特殊的地方,等待释放。

第二个阶段,对那些已经挑出来的对象,调用free函数,释放内存。显然这些挑出来的底层指针指向的对象,已经调用过 析构函数,能释放的已经释放,该调用的已经调用。现在它们就只是指向了有内容,却无需动作的内存空间。这时候, 直接用free释放这个空间即可。

把这两个阶段分开,就可以精确的对每个使用智能指针的对象进行引用计数。可能存在出现负数的情况,但是,我们已经 设置了初始值为1,那么,至多减少1次,就得到0的结果。所以只要检测到0,就可以安全的把对象放在释放列表中了。

这个做法,就好像是其它语言中的GC(垃圾回收机制),不是直接释放内存,而是等到一个特殊的时刻一起处理。 然而,不同于GC,这个做法的好处在于,这个特殊的时刻,可以就是任何对象释放的时刻。也就是说,一个被释放的 对象造成的连带释放,可以在一次释放过程中完全被检测出来。这就相当于立即释放:立即精确的释放所有的相关 对象,而这正是引用计数方法的特点。

引用计数方法可以立即释放对象,但对于循环引用,则或者不能释放,或者造成堆栈溢出。现在,我们把计数减少的 过程和最终释放内存的过程分成两个阶段,那么不能释放或者堆栈溢出都可以被避免。

事实上,既然已经可以精确的得出需要释放的所有底层指针,我们还可以延迟释放。也就是说,先不急着释放, 等需要的时候再释放。而这就和GC非常像了。此外,还有一个更好的特性:既然可以延迟释放,既然那些底层指针 所指向的内存绝对不会再被使用,那么,我们就可以单独用一个线程来作为释放线程。

可以手动启动释放内存的线程,它将会把当前所有需要释放的底层指针都free掉。也可以使用某种定时设计或者对 当前内存使用总量进行监控。在需要的时候,用自动启动的线程来释放内存,或者用监控线程本身来释放内存。

而这些在另一个线程中进行的释放活动,(几乎)完全不会影响其它线程中请求内存的操作,这是因为,可以把 待释放的内存列表单独拷贝出来,而不用共享列表,所以即便上锁,也只是快速拷贝阶段才上锁。这就使得系统 性能可以得到最大的提升。

基本上这就是所谓的RCGC算法。用在C++上显然可以,以相同的原理移植到C语言或者GO语言当然也行。 事实上,也可以把它改装到C#等纯GC系统中,使用引用计数加上独立线程释放内存的方式显然要比 STOP-THE-WORLD高效得多了。

P.S:
在类中,有些成员变量(字段)使用rcgc_shared_ptr管理,有些只是在析构函数中释放的情况下,需要如何处理?
不难想到,由于类对象的析构函数(不是特定智能指针的析构函数)可能被反复调用,那些成员变量可能已经被
delete释放了,但是析构函数还是会再调用一次。所以为了保证安全,应当使用如下方法:

~A(){
    if(this->_ptr_B!=nullptr){
        delete this->_ptr_B;
        this->_ptr_B=nullptr;
    }
}

但若可能,最好都使用rcgc_shared_ptr来管理。

项目地址:https://github.com/yyl-20020115/RCGC

恳请批评指正:

yyl_20050115@hotmail.com

祝好,

杨逸林

 

附:源码

//rcgc_shared_ptr.h
#pragma once
#include<mutex>
#include<vector>
#include<unordered_map>

class rcgc_base {
public:
    static bool SetAutoCollect(bool ac);
    static bool GetAutoCollect();
    static void Collect(bool threading = false, bool join = false);
protected:
    static void* AddRef(void* ptr);
    static void* RelRef(void* ptr);
    static void CollectThread();
    static void Collect(std::vector<void*>& p_wilds);
protected:
    static bool _ac;
    static std::mutex _m;
    static std::unordered_map<void*, size_t> _refs;
    static std::vector<void*> _wilds;
};

template<class PTR>
class rcgc_shared_ptr
    : public rcgc_base
{
public:

    rcgc_shared_ptr(PTR* ptr = nullptr) :_ptr(ptr) {
        AddRef(this->_ptr);
    }
    rcgc_shared_ptr(const rcgc_shared_ptr& src) :_ptr(src._ptr){
        AddRef(this->_ptr);
    }
    //NOTICE: do not use any virtual function to avoid 
    //building virtual function table for this pointer
    //virtual 
    ~rcgc_shared_ptr() {
        this->Dispose();
        if (_ac) {
            Collect();
        }
    }
public:
    void Dispose() {
        if (this->_ptr != nullptr) {
            PTR* _ptr = this->_ptr;
            this->_ptr = nullptr;
            _ptr->~PTR();
            RelRef(_ptr);
        }
    }
    rcgc_shared_ptr& operator = (rcgc_shared_ptr<PTR>& src) {
        if (this->_ptr == src._ptr) {
        }
        else if (src._ptr != nullptr) {
            RelRef(this->_ptr);
            AddRef(this->_ptr = src._ptr);
        }
        return *this;
    }

    PTR& operator*() const {
        return *_ptr;
    }
    PTR* operator->() const {
        return _ptr;
    }
protected:
    PTR* _ptr;
};
//rcgc_shared_ptr.cpp
#include "rcgc_shared_ptr.h"

std::mutex rcgc_base::_m;
bool rcgc_base::_ac = false;
std::unordered_map<void*, size_t> rcgc_base::_refs;
std::vector<void*> rcgc_base::_wilds;

//this relationship is simplified,
//and represented by counting plus one.
void* rcgc_base::AddRef(void* ptr)
{
    if (ptr != nullptr) {
        std::lock_guard<std::mutex> lock(_m);
        auto p = _refs.find(ptr);
        if (p == _refs.end()) {
            _refs.insert(std::make_pair(ptr, 1));
        }
        else {
            p->second++;
        }
        check if RelRef removed the ptr, undo if found
        //if (_wilds.size() > 0 && _wilds.back() == ptr) {
        //    _wilds.pop_back();
        //}
    }
    return ptr;
}

void* rcgc_base::RelRef(void* ptr)
{
    if (ptr != nullptr) {
        std::lock_guard<std::mutex> lock(_m);
        auto p = _refs.find(ptr);
        if (p != _refs.end()) {
            if (--p->second == 0) {
                _refs.erase(p);
                _wilds.push_back(ptr);
            }
        }
    }
    return ptr;
}

bool rcgc_base::SetAutoCollect(bool ac)
{
    return _ac = ac;
}

bool rcgc_base::GetAutoCollect()
{
    return _ac;
}

void rcgc_base::Collect(bool threading, bool join)
{
    if (_wilds.size() > 0) {
        if (threading) {
            std::thread t(CollectThread);
            if (join) {
                t.join();
            }
            else {
                t.detach();
            }
        }
        else {
            Collect(_wilds);
        }
    }
}

void rcgc_base::CollectThread()
{
    std::vector<void*> p_wilds;
    {
        std::lock_guard<std::mutex> lock(_m);
        p_wilds = _wilds;
        _wilds.clear();
    }
    Collect(p_wilds);
}

void rcgc_base::Collect(std::vector<void*>& p_wilds)
{
    if (p_wilds.size() > 0) {
        for (auto p = p_wilds.begin(); p != p_wilds.end(); ++p) {
            if (*p) {
                //delete p->first;
                //NOTICE: use free instead of delete to avoid double calling destructor
                free(*p);
            }
        }
        p_wilds.clear();
    }
}

 

//main.cpp (compile with Visual Studio 2019)

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include "rcgc_shared_ptr.h"

class B;
class A
{
public:
    A() : _ptr_outB1()
        , _ptr_outB2() {}

public:
    rcgc_shared_ptr<A> _ptr_outA1;
    rcgc_shared_ptr<B> _ptr_outB1;
    rcgc_shared_ptr<B> _ptr_outB2;
};
class B
{
public:
    B() :_ptr_outA() {}

public:
    rcgc_shared_ptr<A> _ptr_outA;
};

//Here is a simple method to solve circulating reference problem of shared_ptr without using weak_ptr.
int main()
{
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

    rcgc_base::SetAutoCollect(true);

    rcgc_shared_ptr<A> ptr_A(new A);
    rcgc_shared_ptr<B> ptr_B(new B);

    ptr_A->_ptr_outA1 = ptr_A;
    ptr_A->_ptr_outB1 = ptr_B;
    ptr_A->_ptr_outB2 = ptr_B;
    ptr_B->_ptr_outA = ptr_A;
}

 

//bad_sample.cpp:用以阐释为什么shared_ptr在循环引用的时候会无法正确减去计数值并释放内存。
#include<iostream>
#include<map>
using namespace std;
namespace bad_sample {
	class mshared_base {
	public:
		static map<void*, int> _map;		//静态数据成员需要在类外进行初始化
	};
	template<typename T>
	class mshared_ptr : public mshared_base
	{
	public:
		mshared_ptr(T* ptr = NULL);		//构造方法
		~mshared_ptr();		//析构方法
		mshared_ptr(mshared_ptr<T>& src);		//拷贝构造
		mshared_ptr& operator = (mshared_ptr<T>& src);		//赋值运算符重载
		T& operator*();		//解引用运算符重载
		T* operator->();	//成员运算符重载
	private:
		T* _ptr;
		//错误:不应根据T*建立不同的_map,否则两个不同类的对象不会在同一个
		//map中计数。
		//static map<T*, int> _map;		//静态数据成员需要在类外进行初始化
	};

	map<void*, int> mshared_base::_map;

	template<typename T>
	mshared_ptr<T>::mshared_ptr(T* ptr)		//构造方法
	{
		cout << "mshared_ptr的 CONSTRUCTOR 方法正被调用!:" << ptr << endl;
		_ptr = ptr;
		_map.insert(make_pair(_ptr, 1));
	}

	template<typename T>
	mshared_ptr<T>::~mshared_ptr()		//析构方法
	{
		//错误原因:智能指针释放的时候总是应该释放其指向的对象
		//若不能如此,也应当调用其指向的对象的析构函数
		//但若真的调用,则会产生循环引用的释放问题。
		//所以在调用的时候,不能释放指针指向的内存,而要通过有控制的
		//调用来仅仅减少引用计数。

		cout << "mshared_ptr的 DESTROCTOR 方法正被调用!:" << this->_ptr << endl;
		if (--_map[_ptr] <= 0 && NULL != _ptr)
		{
			delete _ptr;
			_ptr = NULL;
			_map.erase(_ptr);
		}
	}

	template<typename T>
	mshared_ptr<T>::mshared_ptr(mshared_ptr<T>& src)	//拷贝构造
	{
		cout << "mshared_ptr的 CONSTROCTOR-2 方法正被调用!:" << src._ptr << endl;

		_ptr = src._ptr;
		_map[_ptr]++;
	}

	template<typename T>
	mshared_ptr<T>& mshared_ptr<T>::operator=(mshared_ptr<T>& src)		//赋值运算符重载
	{
		cout << "mshared_ptr的 ASSIGN 方法正被调用!:" << this->_ptr << " => " << src._ptr << endl;
		if (_ptr == src._ptr)
		{
			return *this;
		}

		if (--_map[_ptr] <= 0 && NULL != _ptr)
		{
			delete _ptr;
			_ptr = NULL;
			_map.erase(_ptr);
		}

		_ptr = src._ptr;
		_map[_ptr]++;
		return *this;
	}

	template<typename T>
	T& mshared_ptr<T>::operator*()		//解引用运算符重载
	{
		cout << "mshared_ptr的 & 方法正被调用!:" << this->_ptr << endl;

		return *_ptr;
	}

	template<typename T>
	T* mshared_ptr<T>::operator->()		//成员运算符重载
	{
		cout << "mshared_ptr的 -> 方法正被调用!:" << this->_ptr << endl;

		return _ptr;
	}

	class B;    //同文件,从上至下编译,故而需要告诉类A——类B确实存在
	class A
	{
	public:
		virtual ~A() {
			cout << "A 的 DESTROCTOR 方法正被调用!:" << this << endl;

		}
	public:

		mshared_ptr<B>_ptr_B;
	};
	class B
	{
	public:
		virtual ~B() {
			cout << "B 的 DESTROCTOR 方法正被调用!:" << this << endl;
		}
	public:
		mshared_ptr<A>_ptr_A;
	};

	int bad_main()
	{
		mshared_ptr<A>ptr_A(new A);
		mshared_ptr<B>ptr_B(new B);
		ptr_A->_ptr_B = ptr_B;
		ptr_B->_ptr_A = ptr_A;
		return 0;
	}

}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值