상세 컨텐츠

본문 제목

Reference Counting

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 9. 8. 23:24

본문

포인터를 이리 저리 넘겨주는 코드를 작성한 뒤 해당 포인터가 필요없다고 생각해서 지우면 문제가 되는 경우가 많다.

 

class Wraight
{
public:
	int _hp = 150;
	int _posX = 0;
	int _posY = 0;
};

class Missile
{
public:
	void SetTarget(Wraight* target)
	{
		_target = target;
	}

	void Update()
	{
		int posX = _target->_posX;
		int posY = _target->_posY;

		// TODO 쫓아가기
	}

	Wraight* _target = nullptr;
};

int main()
{
	Wraight* wraight = new Wraight();
	Missile* missile = new Missile();
	missile->SetTarget(wraight);

	// 레이스가 피격 당함
	wraight->_hp = 0;
	delete wraight;

	while (true)
	{
		if (missile)
		{
			missile->Update();
		}
	}
}

 

레이스 포인터를 missile에 넘겼는데 레이스가 피격당하여 메모리를 삭제한 코드이다. 크래시가 나서 프로그램이 죽으면 다행이지만 어떤 경우엔 delete한 메모리를 참고하여 쓰레기값을 가져오는 경우도 있다.

 

이런 문제를 해결하기 위해서 Reference count를 사용한다.

RefCounting.h

#pragma once

/*-----------------
	RefCountable
-----------------*/

class RefCountable
{
public:
	RefCountable() : _refCount(1) {  }
	virtual ~RefCountable() { }

	int32 GetRefCount() { return _refCount; }

	int32 AddRef() { return ++_refCount; }
	int32 ReleaseRef()
	{
		int32 refCount = --_refCount;
		if (refCount == 0)
			delete this;

		return refCount;
	}

protected:
	int32 _refCount;
};
class Wraight : public RefCountable
{
public:
	int _hp = 150;
	int _posX = 0;
	int _posY = 0;
};

class Missile : public RefCountable
{
public:
	void SetTarget(Wraight* target)
	{
		_target = target;
		target->AddRef();
	}

	bool Update()
	{
		if (_target == nullptr) { return true; }

		int posX = _target->_posX;
		int posY = _target->_posY;

		// TODO 쫓아가기

		if (_target->_hp == 0)
		{
			_target->ReleaseRef();
			_target = nullptr;
			return true;
		}

		return false;
	}

	Wraight* _target = nullptr;
};

int main()
{
	Wraight* wraight = new Wraight();
	Missile* missile = new Missile();
	missile->SetTarget(wraight);

	// 레이스가 피격 당함
	wraight->_hp = 0;
	wraight->ReleaseRef();
	wraight = nullptr;

	while (true)
	{
		if (missile)
		{
			if (missile->Update())
			{
				missile->ReleaseRef();
				missile = nullptr;
			}
		}
	}

	missile->ReleaseRef();
	missile = nullptr;
}

 

위 코드의 문제는 아래와 같다.

1. AddRef를 수동으로 한다.

2. Multithread 환경에서 제대로 작동하지 않는다.

 

2.를 해결하기 위해 _refCount를 atomic으로 바꿔도 문제를 해결할 수 없다.

예를 들어 SetTarget에서 포인터 복사와 AddRef사이에 다른 쓰레드가 ReleaseRef를 호출할 수 있다.

만약 RefCount가 0이 되면 메모리를 해제하고 _target은 쓰레기 값을 참조하게 된다.

 

문제를 해결하기 위해 직접 smart pointer를 구현해보자.

RefCounting.h

/*-----------------
	SharedPtr
-----------------*/

template<typename T>
class TSharedPtr
{
public:
	TSharedPtr() {}
	TSharedPtr(T* ptr) { Set(ptr); }

	// 복사
	TSharedPtr(const TSharedPtr& rhs) { Set(rhs._ptr); }
	// 이동
	TSharedPtr(TSharedPtr&& rhs) { _ptr = rhs._ptr; rhs._ptr = nullptr; }
	// 상속 관계 복사
	template<typename U>
	TSharedPtr(const TSharedPtr<U>& rhs) { Set(static_cast<T*>(rhs._ptr)); }

	~TSharedPtr() { Release(); }

public:
	// 복사 연산자
	TSharedPtr& operator=(const TSharedPtr& rhs)
	{
		if (_ptr != rhs._ptr)
		{
			Release();
			Set(rhs._ptr);
		}
		return *this;
	}

	// 이동 연산자
	TSharedPtr& operator=(TSharedPtr&& rhs)
	{
		Release();
		_ptr = rhs._ptr;
		rhs._ptr = nullptr;
		return *this;
	}

	bool		operator==(const TSharedPtr& rhs) const { return _ptr == rhs._ptr; }
	bool		operator==(T* ptr) const { return _ptr == ptr; }
	bool		operator!=(const TSharedPtr& rhs) const { return _ptr != rhs._ptr; }
	bool		operator!=(T* ptr) const { return _ptr != ptr; }
	bool		operator<(const TSharedPtr& rhs) const { return _ptr < rhs._ptr; }
	T*			operator*() { return _ptr; }
	const T*	operator*() const { return _ptr; }
				operator T* () const { return _ptr; }
	T*			operator->() { return _ptr; }
	const T*	operator->() const { return _ptr; }

	bool IsNull() { return _ptr == nullptr; }


private:
	void Set(T* ptr)
	{
		_ptr = ptr;
		if (ptr)
			ptr->AddRef();
	}

	void Release()
	{
		if (_ptr != nullptr)
		{
			_ptr->ReleaseRef();
			_ptr = nullptr;
		}
	}

private:
	T* _ptr = nullptr;
};
using WraightRef = TSharedPtr<Wraight>;
class Missile : public RefCountable
{
public:
	void SetTarget(WraightRef target)
	{
		_target = target;
		//target->AddRef();
	}

	bool Update()
	{
		if (_target == nullptr) { return true; }

		int posX = _target->_posX;
		int posY = _target->_posY;

		// TODO 쫓아가기

		if (_target->_hp == 0)
		{
			//_target->ReleaseRef();
			_target = nullptr;
			return true;
		}

		return false;
	}

	WraightRef _target = nullptr;
};


using MissileRef = TSharedPtr<Missile>;

int main()
{
	WraightRef wraight(new Wraight());
	wraight->ReleaseRef();
	MissileRef missile(new Missile());
	missile->ReleaseRef();

	missile->SetTarget(wraight);

	// 레이스가 피격 당함
	wraight->_hp = 0;
	//wraight->ReleaseRef();
	wraight = nullptr;

	while (true)
	{
		if (missile)
		{
			if (missile->Update())
			{
				//missile->ReleaseRef();
				missile = nullptr;
			}
		}
	}

	//missile->ReleaseRef();
	missile = nullptr;
}

 

TSharedPtr이 복사하면서 RefCounting을 1올리기 때문에 이전과 같은 문제는 발생하지 않는다.

 

 

1. RefCounting을 사용하는 이유는?

- 객체의 생성, 소멸 시기를 관리하기 위함.

2. RefCounting의 문제 2가지는?

- 수동으로 생성, 소멸 주기를 관리해야 함.(AddRef, ReleaseRef)

- 멀티쓰레딩 환경에서 동작히지 않음.

3. TSharedPtr 사용하면 RefCounting 사용했을 때의 문제가 생기지 않는 이유는?

- 래핑 클래스(TSharedPtr)가 생성, 소멸 주기를 대신 관리하기 때문.

- 함수 파라메타로 전달할 때, 복사 생성자를 부르면서 최소한 1의 refcount는 유지하기 때문.

4. TSharedPtr ref count 줄이는 방법과 그 이유는?

- TSharedPtr에 nullptr 을 할당

- 생성자와 복사 연산자를 차례로 실행하면서 refcount를 줄인다.

 

참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)

'똑똑한 개발 > C++ 게임개발' 카테고리의 다른 글

Allocator  (0) 2021.09.11
스마트 포인터  (0) 2021.09.10
Deadlock 탐지  (0) 2021.09.06
Read-Writer lock  (0) 2021.09.03
ThreadManager  (0) 2021.09.01

관련글 더보기

댓글 영역