728x90

Reference Counting 를 알고있어야 합니다. 잘 모르신다면 이쪽으로....

 

개요

순환 참조 문제는 비단 Reference Counting 뿐만이 아니라 다양한 영역에서 이를 피하는것이 매우 중요합니다.

 

설계적 관점에서, 서로 참조를 하는 두 객체가 있다면 의존 관계가 양방향이 되고

 

의존성(Dependency)이 커지기 때문에 코드 관리에 어려움이 생깁니다. (스파게티 요리사!)

 

멀티 스레드( or 프로세스 ) 환경에서는 Resource를 점유한 상태로 새 Resource의 요청이 "순환"하는 경우

 

교착 상태(Dead Lock)에 빠져 무한 대기 상태에 빠지게 됩니다.

 

다시 본론으로 와서, 순환 참조는 항상 안좋다! 라는 것을 기억하시고 살펴볼까요??

 

 

만약 두 객체 다음과 같이 서로를 참조하고 있다면 어떻게 될까요?

<그림1> 순환참조 예

바로 코드로 작성하여 확인 해보죠.

RefCounted 코드는 저번 Reference Counting 내용의 non-Intrusive 구현을 사용하였습니다. (대입 연산자만 추가)

class ObjectA
{
public:
	ObjectA(void) { std::cout << "::A Initailized!" << std::endl; }
	~ObjectA(void) { std::cout << "::A Deinitialized!" << std::endl; }
	RefCounted<ObjectB> referenceB;
};

class ObjectB
{
public:
	ObjectB(void) { std::cout << "::B Initailized!" << std::endl; }
	~ObjectB(void) { std::cout << "::B Deinitialized!" << std::endl; }
	RefCounted<ObjectA> referenceA;
};

int main(void)
{
	RefCounted<ObjectA> countA(new ObjectA);
	RefCounted<ObjectB> countB(new ObjectB);

	std::cout << "countA : " << countA.getCount() << std::endl;
	std::cout << "countB : " << countB.getCount() << std::endl;

	// 여기서 순환 참조 발생!
	countA.get()->referenceB = countB;
	countB.get()->referenceA = countA;

	std::cout << "countA : " << countA.getCount() << std::endl;
	std::cout << "countB : " << countB.getCount() << std::endl;

	return 0;
}

결과:

===========================================

::A Initailized!
::B Initailized!
countA : 1
countB : 1
countA : 2
countB : 2

===========================================

 

결과에서 알 수 있듯이 ObjectA와 ObjectB가 해제되지 않습니다. 해제 과정은 다음과 같은데요.

 

  1. countB가 해제 되며 ObjectB의 count는 1이됩니다. (stack이라 countB가 먼저 해제 됩니다!)

  2. countA가 해제 되며 ObjectA의 count는 1이 됩니다.

  3. ObjectB와 ObjectA를 강제로 해제하지 않는 이상 서로에 대한 참조 카운트가 내려가지 않아

     해제가 되지 않습니다.

 

메모리 관리를 용이하게 하려고 Reference Count를 사용했는데 메모리가 해제 되지 않아버리면 정말 난감 하겠죠?

 

이를 해결 하기 위해 많은 방법이 있지만, 1가지 방법을 소개하고자 합니다.

 

Automatic Reference Counting (Weak Reference)

 

용어 자체는 swift의 출현과 함께 나온 듯 합니다. Objective-C 시절 MRC (Manual Referenc Counting)에서 카운트를

 

내리거(해제)나 올리기(획득) 위해 개발자 직접 Releas, Retain과 같은 코드를 작성했다가, ARC가 출현하면서

 

컴파일타임에 컴파일러가 자동으로 추가해줘서 더이상 적지 않아도 되니 ARC다!! 라는것에 기인한다고 합니다.

(iOS 개발 해본 적은 없어 정확하지 않다면 알려주세요 ㅠㅠ)

 

사실 이게 중요한 것이 아니라, 눈여겨 봐야할건 바로 참조의 수준을 나눈 것입니다.

 

strong, weak 참조로 나누어 순환 참조를 해결하였습니다!!!

 

"약한 참조"는 실제 객체의 참조 카운트를 올리지 않는 것입니다!!

 

어? 그럼 c++이면 걍 한쪽은 raw pointer를 들고 있으면 되지 않나요?

class ObjectA
{
public:
	ObjectA(void) { std::cout << "::A Initailized!" << std::endl; }
	~ObjectA(void) { std::cout << "::A Deinitialized!" << std::endl; }
	ObjectB* referenceB;
};

class ObjectB
{
public:
	ObjectB(void) { std::cout << "::B Initailized!" << std::endl; }
	~ObjectB(void) { std::cout << "::B Deinitialized!" << std::endl; }
	RefCounted<ObjectA> referenceA;
};

이런식으로???

 

좋은 시도지만 애석하게도 dangling이 발생할 수 있습니다. 만약 다른 곳에서 ObjectB의 참조 카운트가 0이 되어

 

해제되었는데 referenceB를 사용하려 한다면 valid한지 알 길이 없습니다.

 

raw pointer만으로 약한 참조를 흉내낼 수 있지만 해당 객체를 사용할 수 있는지 없는지도 알아야 겠죠???

 

c++에서 그 구현체가 바로 weak_ptr 입니다.

 

구현

non-Intrusive 참조 카운터를 구현하며 count를 단순히 object의 count만 만들었는데

 

이제는 count가 두개가 됩니다! 기존 object의 count는 strong count, 약한 참조용으로 weak count가 추가되어

 

"약한 참조자"가 참조를 하는 경우 strong count가 아닌 weak count가 올라갑니다.

 

따라서 피참조자 Object는 strong count가 0이 되면 해제되며, Count Object는

 

strong count도 0이고 weak count가 0이 되면 해제됩니다.

 

: 실제 count를 관장하는 count helper class

class RefCountHelper
{
public:
	RefCountHelper(void)
		: _strongCount(0)
		, _weakCount(0)
	{
	}

	int getStrongCount(void)
	{
		return _strongCount;
	}

	void addStrongCount(void)
	{
		++_strongCount;
	}

	int releaseStrongCount(void)
	{
		return --_strongCount;
	}

	int getWeakCount(void)
	{
		return _weakCount;
	}

	void addWeakCount(void)
	{
		++_weakCount;
	}

	int releaseWeakCount(void)
	{
		return --_weakCount;
	}

private:
	int		_strongCount;
	int		_weakCount;
};

: Count Helper class를 사용하여 strong count를 제어하도록 수정된 RefCouted

template<class Type>
class RefCounted
{
public:
	template<class Type> friend class WeakRefCounted;

	RefCounted(void)
		: _pObject(nullptr)
		, _pRefCount(nullptr)
	{
	}

	RefCounted(Type* pObject)
		: _pObject(pObject)
		, _pRefCount(nullptr)
	{
		addCount();
	}

	RefCounted(RefCounted<Type>& pObject)
		: _pObject(pObject.get())
		, _pRefCount(pObject._pRefCount)
	{
		addCount();
	}

	RefCounted<Type>& operator = (const RefCounted<Type>& lvalue)
	{
		_pObject = lvalue._pObject;
		_pRefCount = lvalue._pRefCount;

		addCount();
		return *this;
	}

	~RefCounted(void)
	{
		if (nullptr != _pObject)
		{
			reset();
		}
	}

	Type* get(void)
	{
		return _pObject;
	}

	int getCount(void) const
	{
		return _pRefCount->getStrongCount();
	}

private:
	void reset(void)
	{
		if (0 >= releaseCount())
		{
			delete _pObject;
		}
	}

	void addCount(void)
	{
		if (nullptr == _pRefCount)
		{
			_pRefCount = new RefCountHelper();
		}
		_pRefCount->addStrongCount();
	}

	int releaseCount(void)
	{
		return _pRefCount->releaseStrongCount();
	}

private:
	Type*			_pObject;
	RefCountHelper*	_pRefCount;
};

: Count Helper class를 사용하여 weak count를 제어하는 WeakRefCounted

template<class Type>
class WeakRefCounted
{
public:
	WeakRefCounted(void)
		: _pObject(nullptr)
		, _pRefCount(nullptr)
	{
	}

	WeakRefCounted(RefCounted<Type>& pObject)
		: _pObject(pObject.get())
		, _pRefCount(pObject._pRefCount)
	{
		addCount();
	}

	WeakRefCounted<Type>& operator = (const RefCounted<Type>& lvalue)
	{
		_pObject = lvalue._pObject;
		_pRefCount = lvalue._pRefCount;

		addCount();
		return *this;
	}

	~WeakRefCounted(void)
	{
		if (nullptr != _pObject)
		{
			reset();
		}
	}

	Type* get(void)
	{
		if (0 >= _pRefCount->getStrongCount())
		{
			// 이미 해제 되었다.
			return nullptr;
		}

		return _pObject;
	}

	int getCount(void) const
	{
		return _pRefCount->getWeakCount();
	}

private:
	void reset(void)
	{
		if (0 >= releaseCount() && 0 >= _pRefCount->getStrongCount())
		{
			delete _pRefCount;
		}
	}

	void addCount(void)
	{
		_pRefCount->addWeakCount();
	}

	int releaseCount(void)
	{
		return _pRefCount->releaseWeakCount();
	}

private:
	Type*			_pObject;
	RefCountHelper*	_pRefCount;
};

WeakRefCounted의 get 메소드를 보시면 strong count가 0이면 nullptr를 반환 합니다. 따라서 이미 해제되었는지

 

알 수 있어서 매우 "스마트"합니다! 이제 ObjectA, ObjectB의 순환 참조 문제를 해결할 수 있습니다!

class ObjectA
{
public:
	ObjectA(void) { std::cout << "::A Initailized!" << std::endl; }
	~ObjectA(void) { std::cout << "::A Deinitialized!" << std::endl; }
	void print(void) { std::cout << "print called!" << std::endl; }
	WeakRefCounted<ObjectB> referenceB; // 약한 참조!
};

class ObjectB
{
public:
	ObjectB(void) { std::cout << "::B Initailized!" << std::endl; }
	~ObjectB(void) { std::cout << "::B Deinitialized!" << std::endl; }
	RefCounted<ObjectA> referenceA;
};

int main(void)
{
	RefCounted<ObjectA> countA(new ObjectA);
	RefCounted<ObjectB> countB(new ObjectB);

	std::cout << "countA : " << countA.getCount() << std::endl;
	std::cout << "countB : " << countB.getCount() << std::endl;

	countA.get()->referenceB = countB;
	countB.get()->referenceA = countA;

	std::cout << "countA : " << countA.getCount() << std::endl;
	std::cout << "countB : " << countB.getCount() << std::endl;

	return 0;
}

결과:

===========================================

::A Initailized!
::B Initailized!
countA : 1
countB : 1
countA : 2
countB : 1
::B Deinitialized!
::A Deinitialized!

===========================================

 

결론

weak_ptr을 적극적으로 쓰자!

728x90

'프로그래밍 > C++' 카테고리의 다른 글

Adaptor Pattern  (0) 2021.06.30
Factory Pattern - Simple Factory  (1) 2021.02.21
[C++] Reference Counting  (2) 2020.11.04
힙(Heap)과 완전 이진 트리(Complete binary tree)  (1) 2019.10.31
[c] SHA-1 구조 및 코드  (2) 2017.12.09
728x90

개요

Reference Counting은 객체의 소유권 관리( = 라이프 사이클 )의 방법 중 하나로 객체를 참조(포인팅) 하고 있는

 

횟수를 추적하여 그 횟수가 0이 되면 메모리에서 해제(소멸)한다.

 

대부분의 Managed Language (python, c#, swift등 메모리 관리를 직접 하지 않는 언어 ) 에서 널리 사용되고 있다.

 

Unmanaged Language (c, c++등) 에서도 결국 객체 관리를 위해 (또한 프로그래머의 실수를 줄이기 위해) 

 

Reference Counting을 구현하여 사용하고 있다...

 

장점

  - 메모리를 직접 해제하는 번거로움이 사라진다. (잘가라 delete)

  - 객체의 소유권을 공유할 수 있다

  - 빠르다. (객체 관리 매커니즘이 비교적 단순하다. feat. Garbage Collection...)

단점

  - 순환 참조 문제가 있다.

 

구현

c++ 에서의 구현방식에는 크게 두가지가 있다.

Intrusive Reference Counting (침습성 참조 카운팅)

  - 객체에 대한 참조 카운트가 "내장" 되어있다.

  - 참조 카운트 매커니즘을 객체가 제공해야한다.

  - 메모리사용은 보통 "비침습성"에 비해 작다.

  - object 해제시 count 정보도 날아가기 때문에 약한 참조를 구현할 수 없다. (=> 순환참조 문제)

<그림1> Intrusive Reference Counting 예시

!주의 : 이해를 돕기 위한 코드로 현업에서 사용시 뚝배기가 깨집니다!!

#include <iostream>

class MyObject
{
public:
	MyObject(void) : _refCount(0) { std::cout << ":: Initailized!" << std::endl; }
	~MyObject(void) { std::cout << ":: Deinitialized!" << std::endl; }

	int getCount(void)
	{
		return _refCount;
	}

	void addCount(void)
	{
		++_refCount;
	}

	void releaseCount(void)
	{
		if (0 >= --_refCount)
		{
			delete this;
		}
	}

private:
	int _refCount;
};

template<class Type>
class RefCounted
{
public:
	RefCounted(Type* pObject)
		: _pObject(pObject)
	{
		_pObject->addCount();
	}

	RefCounted(RefCounted<Type>& pObject)
		: _pObject(pObject.get())
	{
		_pObject->addCount();
	}

	~RefCounted(void)
	{
		reset();
	}

	Type* get(void)
	{
		return _pObject;
	}

private:
	void reset(void)
	{
		_pObject->releaseCount();
	}

private:
	Type* _pObject;
};

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

	RefCounted<MyObject> count2(count1);
	std::cout << "count1 : " << count1.get()->getCount() << std::endl;
	std::cout << "count2 : " << count2.get()->getCount() << std::endl;

	return 0;
}

결과:

===========================================

:: Initailized!
count1 : 1
count1 : 2
count2 : 2
:: Deinitialized!

===========================================

 

non-Intrusive Reference Counting (비침습성 참조 카운팅; c++의 shared_ptr)

  - 객체에 대한 참조 카운트를 따로 관리한다.

  - 참조 카운트에 대한 포인터가 추가되어 보통 메모리사용이 "침습성"에 비해 크다

  - count 정보가 따로 있어 약한 참조를 구현할 수 있다.

<그림2> non-Intrusive Reference Counting 예시

!주의 : 이해를 돕기 위한 코드로 현업에서 사용시 뚝배기가 깨집니다!!

#include <iostream>

class MyObject
{
public:
	MyObject(void) { std::cout << ":: Initailized!" << std::endl; }
	~MyObject(void) { std::cout << ":: Deinitialized!" << std::endl; }
};

template<class Type>
class RefCounted
{
public:
	RefCounted(Type* pObject)
		: _pObject(pObject)
		, _pRefCount(nullptr)
	{
		addCount();
	}

	RefCounted(RefCounted<Type>& pObject)
		: _pObject(pObject.get())
		, _pRefCount(pObject._pRefCount)
	{
		addCount();
	}

	~RefCounted(void)
	{
		reset();
	}

	Type* get(void)
	{
		return _pObject;
	}

	int getCount(void) const
	{
		return *_pRefCount;
	}

private:
	void reset(void)
	{
		if (0 >= releaseCount())
		{
			delete _pObject;
			delete _pRefCount;
		}
	}

	void addCount(void)
	{
		if (nullptr == _pRefCount)
		{
			_pRefCount = new int;
			(*_pRefCount) = 0;
		}
		++(*_pRefCount);
	}

	int releaseCount(void)
	{
		return --(*_pRefCount);
	}

private:
	Type*	_pObject;
	int*	_pRefCount;
};

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

	RefCounted<MyObject> count2(count1);
	std::cout << "count1 : " << count1.getCount() << std::endl;
	std::cout << "count2 : " << count2.getCount() << std::endl;

	return 0;
}

결과:

===========================================

:: Initailized!
count1 : 1
count1 : 2
count2 : 2
:: Deinitialized!

===========================================

결론

우리의 C++을 모던하게 쓰자! 적극적으로 shared_ptr을 쓰자!

728x90

+ Recent posts