[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
恳请批评指正:
祝好,
杨逸林
附:源码
//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;
}
}