본문 바로가기

알게 된 것

[알게 된 것] - reference counting(참조 횟수 계산)

스마트 포인터를 배우기 전에 reference counting 기법에 대해 먼저 배웠으나 스마트 포인터에 대한 글을 쓰고 이 글을 다시 쓰게 되었다. 스마트 포인터가 레퍼런스 카운팅 기반으로 만들어졌으므로 이에 대해서도 간략히 정리하려한다.

 

 

 

https://ko.wikipedia.org/wiki/%EC%B0%B8%EC%A1%B0_%ED%9A%9F%EC%88%98_%EA%B3%84%EC%82%B0_%EB%B0%A9%EC%8B%9D

 

참조 횟수 계산 방식 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 참조 횟수 계산 방식(reference counting)은 메모리를 제어하는 방법 중 하나로, 쓰레기 수집의 한 방식이다. 구성 방식은 단순하다. 어떤 한 동적 단위(객체, Object)가

ko.wikipedia.org

struct object
{
    int ref;
};

struct object* object_new(void)
{
    struct object* p=malloc(sizeof(struct object));
    p->ref=1;
    return p;
}

int object_ref(struct object* p)
{
    return (p->ref++);
}

int object_unref(struct object* p)
{
    if ((p->ref--)!=0)
        return p->ref;
    else
    {
        free(p);
        return 0;
    }
}

 

 

C기반의 reference counting 구조체이다. object_ref(), object_unref()를 통해 reference count를 증가, 감소 시키며 최종적으로 0이 되었을 때 할당받은 메모리를 반환한다.

 

 

위의 코드는 C기반이지만 C++에서는 클래스와 템플릿이 존재하기 때문에 좀더 사용하기 편리한 reference counting을 구현할 수 있다.

 

 

#include <iostream>

class testObject
{
public:

	testObject(void) : _refCount(0) { std::cout << "\ntestObject instructor 호출" << std::endl; }
	~testObject(void) { std::cout << "\ntestObject destructor 호출" << std::endl; }

	int getCount(void)
	{
		return _refCount;
	}

	void addCount(void)
	{
		++_refCount;
	}

	void releaseCount(void)
	{
		// refer count가 0이 되면 delete
		if (0 >= --_refCount)
		{
			delete this;
		}
	}

private:
	int _refCount;
};

template<class T>
class RefCounted
{
public:
	RefCounted(T* object) : _object(object)
	{
		std::cout << "\nRefCount instructor 호출" << std::endl;
		_object->addCount();
	}

	RefCounted(RefCounted<T>& object) : _object(object.getObj())
	{
		std::cout << "\nRefCount instructor 호출" << std::endl;
		_object->addCount();
	}

	~RefCounted(void)
	{
		std::cout << "\nRefCount destructor 호출" << std::endl;
		subCount();
	}

	T* getObj(void)
	{
		return _object;
	}

private:
	void subCount(void)
	{
		_object->releaseCount();
	}

private:
	T* _object;
};

int main(void)
{
	RefCounted<testObject> count1(new testObject);
	std::cout << "count1 : " << count1.getObj()->getCount() << std::endl;

	{
		// 한 번더 참조하여 reference count 1증가
		RefCounted<testObject> count2(count1);
		std::cout << "count1 : " << count1.getObj()->getCount() << std::endl;
		std::cout << "count2 : " << count2.getObj()->getCount() << std::endl;
	}

	// count2가 사라지며 refcount 1감소
	std::cout << "count1 : " << count1.getObj()->getCount() << std::endl;


	return 0;
}

 

실행 결과

 


testObject instructor 호출

RefCount instructor 호출
count1 : 1

RefCount instructor 호출
count1 : 2
count2 : 2

RefCount destructor 호출
count1 : 1

RefCount destructor 호출

testObject destructor 호출

 

우선 count1이라는 객체를 생성하여 해당 객체가 가리키는 객체의 reference count가 1이 된것을 알 수 있다. 그 다음으로 count2를 생성하고 count1이 가리키는 객체를 가리키게 하였다. 이제 count1과 count2가 가리키는 객체가 동일하므로 해당 testObject의 reference count가 2가 괴는 것을 확인할 수 있다.

 

 

count2가 생성된 scope를 벗어나면서 count2의 desturctor가 호출되었고 reference count가 1감소하였다. 중요한 것은 count2가 사라지면서 testObject의 releaseCount()를 호출하였으나 아직 count1이 해당 객체를 가리키고 있으므로(reference count가 0이 아니므로) testObject를 반환하지 않았다는 것이다.

 

 

마지막으로 main함수가 scope를 벗어나면서 RefCount의 destructor를 호출하여 객체의 reference count가 0으로 감소하여 프로그램이 종료되면서 할당받았던 testObject의 메모리를 반환하며 destructor가 호출되는 것을 알 수 있다.

 

 

 

 

 

- reference count 구현 시 circular reference 문제 발생에 관해

 

reference counting 구현 시, 순환 참조 문제를 주의 해야한다. 예시를 위해 아래와 같이 코드를 변경하여 순환 참조 문제가 일어나게끔 하였다.

 

#include <iostream>


template<class T>
class RefCounted
{
public:
	RefCounted(T * object) : _object(object)
	{
		std::cout << "\nRefCount instructor 호출" << std::endl;
		_object->addCount();
	}

	RefCounted(RefCounted<T> & object) : _object(object.getObj())
	{
		std::cout << "\nRefCount instructor 호출" << std::endl;
		_object->addCount();
	}

	~RefCounted(void)
	{
		std::cout << "\nRefCount destructor 호출" << std::endl;
		subCount();
	}

	T* getObj(void)
	{
		return _object;
	}

private:
	void subCount(void)
	{
		_object->releaseCount();
	}

private:
	T* _object;
};

class testObject
{
public:

	testObject(void) : _refCount(0) { std::cout << "\ntestObject instructor 호출" << std::endl; }
	~testObject(void) { std::cout << "\ntestObject destructor 호출" << std::endl; }

	int getCount(void)
	{
		return _refCount;
	}

	void addCount(void)
	{
		++_refCount;
	}

	void releaseCount(void)
	{
		// refer count가 0이 되면 delete
		if (0 >= --_refCount)
		{
			delete this;
		}
	}

	void setPartner(testObject* obj)
	{
		RefCounted<testObject> * partner = new RefCounted<testObject>(obj);
		this->_partner = partner;
	}

private:
	int _refCount;
	RefCounted<testObject> * _partner;

};

int main(void)
{
	RefCounted<testObject> count1(new testObject);
	std::cout << "count1 : " << count1.getObj()->getCount() << std::endl;

	RefCounted<testObject> count2(new testObject);
	std::cout << "count2 : " << count2.getObj()->getCount() << std::endl;
	
	// 순환 참조 문제 발생
	count1.getObj()->setPartner(count2.getObj());
	count2.getObj()->setPartner(count1.getObj());

	std::cout << "count1 : " << count1.getObj()->getCount() << std::endl;
	std::cout << "count2 : " << count2.getObj()->getCount() << std::endl;

	return 0;
}

 

실행 결과

 


testObject instructor 호출

RefCount instructor 호출
count1 : 1

testObject instructor 호출

RefCount instructor 호출
count2 : 1

RefCount instructor 호출

RefCount instructor 호출
count1 : 2
count2 : 2

RefCount destructor 호출

RefCount destructor 호출

 

testObject에 partner라는 멤버를 추가하였고 클래스 객체 2개를 생성하여 서로를 참조하도록하여 순환참조 문제를 발생시켰다. 실행 결과를 보면 testObject에 대한 destructor가 호출되지 않았고, 결과적으로 할당받은 메모리가 해제되지 않아 메모리 누수가 일어났다는 것을 확인할 수 있다.

 

 

 

 

https://teachingforme.tistory.com/19

 

[알게 된 것] circular reference(순환 참조) 문제

java나 c# 같은 언어는 더 이상 접근할 방법이 없는 메모리에 대해 GC가 알아서 해당 메모리를 해제(관리)한다. C, C++같은 경우 GC가 없기 때문에 동적으로 할당한 메모리에 대해 직접 delete를 이용하

teachingforme.tistory.com

 

 

왜 이러한 순환참조 문제가 발생했는지는 이전에 작성한 글에서 정리하였다.

 

 

 

 

현재 코드의 구조는 위와 같다. 정리하자면 현재 testObject라는 객체를 refCounted라는 클래스를 이용하여 reference counting 하여 관리할 것이며 reference count에 대한 정보는 멤버 객체는 testObject라는 객체의 멤버로써 관리되고 있다.

 

 

reference count 에 대한 정보는 굳이 관리 대상인 testObject에서 가지고 있을 필요가 없다. 또한 순환 참조문제를 해결하기 위해 std::weak_ptr과 같이 refCount를 증가 시키지 않고 참조하게끔 구현 할 수가 없다.

 

 

결론은 이거다. refCount를 refCounted에서 관리를 하고, 이렇게 함으로써 refCount를 증가시키지 않고 참조가 가능하도록 구현하여 순환 참조 문제를 해결 할 수 있다. 사실 이것이 std::weak_ptr이 사용되는 이유다.

shared_ptr도 reference count에 대한 정보는 따로 관리 되기 때문에 weak_ptr을 통해서 reference count를 증가시키지 않고 참조할 수 있다.

 

 

 

 

 

#include <iostream>



class testObject
{
public:

	testObject(void) { std::cout << "\ntestObject instructor 호출" << std::endl; }
	~testObject(void) { std::cout << "\ntestObject destructor 호출" << std::endl; }

};


template<class T>
class RefCounted
{
public:
	RefCounted(T * object) : _object(object)
	{
		std::cout << "\nRefCount instructor 호출" << std::endl;
		addCount();
	}

	RefCounted(RefCounted<T> & object) : _object(object.getObj()), _refCount(object.getCount())
	{
		std::cout << "\nRefCount instructor 호출" << std::endl;
		addCount();
	}

	~RefCounted(void)
	{
		std::cout << "\nRefCount destructor 호출" << std::endl;
		releaseCount();
	}

	T* getObj(void)
	{
		return _object;
	}

	int getCount(void)
	{
		return _refCount;
	}

	void addCount(void)
	{
		++_refCount;
	}

	void releaseCount(void)
	{
		// refer count가 0이 되면 delete
		if (0 >= --_refCount)
		{
			delete _object;
		}
	}

	void setPartner(T * obj)
	{
		_parnter = obj;
		// 객체 내부의 참조에 대해서는 refCount를 증가시키지 않는다. -> 순환 참조 문제를 피하기 위해서
	}

	T* getPartner(void)
	{
		if (_parnter == nullptr) // 이미 삭제된 객체라면 null반환
		{
			return nullptr;
		}

		return _parnter;
	}

private:
	T* _object;
	int _refCount;
	T* _parnter = nullptr;
};



int main(void)
{
	RefCounted<testObject> count1(new testObject);
	std::cout << "count1 : " << count1.getCount() << std::endl;

	RefCounted<testObject> count2(new testObject);
	std::cout << "count2 : " << count2.getCount() << std::endl;
	
	
	count1.setPartner(count2.getObj());
	count2.setPartner(count1.getObj());

	std::cout << "\ncount1 : " << count1.getCount() << std::endl;
	std::cout << "count2 : " << count2.getCount() << std::endl;

	return 0;
}

 

실행 결과

 


testObject instructor 호출

RefCount instructor 호출
count1 : 1

testObject instructor 호출

RefCount instructor 호출
count2 : 1

count1 : 1
count2 : 1

RefCount destructor 호출

testObject destructor 호출

RefCount destructor 호출

testObject destructor 호출

 

관리할 대상이 되는 testObject와 reference count에 대한 정보를 분리하고, 내부에서 참조하는 포인터도 refCounted가 소유하도록 코드를 변경하였다. 실행 결과에서 알 수 있듯이 프로그램이 종료되면서 할당한 모든 메모리를 제대로 반환 하고 있음을 확인할 수 있다.

 

 

reference counting에 대한 이론적인 내용은 크게 어려울 것이 없다. 이번에 코드를 작성하면서 눈여겨 봐야 할점이라고 느꼈던 것은 내부적으로 다른 클래스를 참조해야 하는 경우 refCount를 증가시키지 않았다는 점, destructor 호출 시 partner 객체에 대한 메모리는 delete하지 않는다는 점 이었다고 생각한다. partner 객체또한 별도의 refCount에 대해 관리되고 있는 객체이기 때문에 관여해서는 안된다. 사실 이부분이 순환 참조 문제를 해결하는 핵심적인 부분이었다고 생각한다.

 

 

참고로 위의 코드는 reference counting 구현 및 순환 참조 문제 재현을 위해 두서없이 작성한 코드이므로 실전에서는 사용해서는 안될 코드이다. (오류를 잡거나 멀티쓰레딩 환경에서 동작하기 위해서 추가해야할 코드들이 많다.)