KoreanFoodie's Study
[C++ 게임 서버] 2-1. Reference Counting 본문
[C++ 게임 서버] 1-25. Reference Counting
핵심 :
1. 멀티 쓰레드 환경에서, 생 포인터를 사용하는 것은 언제나 위험하다. 의도치 않게 delete 된 포인터를 사용하려고 할 수도 있기 때문이다.
2. 일반적으로는 Smart Pointer 를 통해 이런 문제를 해결하는데, 핵심적인 부분은 결국 Reference Count 를 체크하여 nullptr 에 접근하는 것을 막는 것이다! 물론 SharedPtr 까지 제대로 써 주어야 문제를 막을 수 있다.
멀티쓰레드 프로그래밍을 할 때, 생 포인터를 사용하게 되면 의도치 않게 nullptr 에 접근하는 경우를 맞딱뜨리게 된다.
아래의 예시를 보자. 레이스와 터렛이 있다고 하고, 터렛이 레이스를 타겟팅하여 격추 시키는 것을 상상해 보자! 😆
미사일과 터렛의 클래스는 다음과 같다 :
class Wraight : public RefCountable
{
public:
int _hp = 150;
int _posX = 0;
int _posY = 0;
};
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>;
위에서 레이스와 미사일은 RefCount 를 상속받는데.. 이 녀석은 다음과 같다 :
/*---------------
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:
atomic<int32> _refCount;
};
간단히 말해서, RefCountable 이라는 클래스를 상속받은 녀석은, _refCount 를 통해 객체의 수명이 관리되게 된다.
그리고 _refCount 는 atomic 하게 관리되는데...
이제 실제로 미사일 터렛이 레이스를 격추하는 main 함수를 보자.
int main()
{
WraightRef wraight(new Wraight());
wraight->ReleaseRef();
MissileRef missile(new Missile());
missile->ReleaseRef();
missile->SetTarget(wraight);
// 레이스가 피격 당함
wraight->_hp = 0;
//delete wraight;
//wraight->ReleaseRef();
wraight = nullptr;
while (true)
{
if (missile)
{
if (missile->Update())
{
//missile->ReleaseRef();
missile = nullptr;
}
}
}
//missile->ReleaseRef();
missile = nullptr;
//delete missile;
}
여기서, 미사일이 레이스를 SetTarget 하는 부분을 보면...
만약 기존 코드에서, WraightRef 가 TSharedPtr<Wraight> 가 아니어서, 아래와 같았다면 어떤 일이 발생할까?
void SetTarget(Wraight target)
{
_target = target;
// 중간에 개입 가능
target->AddRef();
}
미사일은 레이스 target 을 참조하니, 레이스의 포인터인 target 의 포인터가 하나 더 늘어나야할 것이다.
그런데, _target = target 이후에, 다른 쓰레드가 target 을 delete 해 버린다면, target->AddRef() 에서는 크래시가 발생할 것이다!
이런 문제를 막기 위해, TSharedPtr 를 사용하면 문제를 해결할 수 있다. 일단 자체적으로 만든 TSharedPtr 의 정의를 보자 😄
/*---------------
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:
inline void Set(T* ptr)
{
_ptr = ptr;
if (ptr)
ptr->AddRef();
}
inline void Release()
{
if (_ptr != nullptr)
{
_ptr->ReleaseRef();
_ptr = nullptr;
}
}
private:
T* _ptr = nullptr;
};
위에서 처럼, TSharedPtr<Wraight> 를 사용하게 되면, 아까 SetTarget 에서 있던 문제가 사라진다.
void SetTarget(WraightRef target)
{
_target = target;
// 중간에 개입 가능
//target->AddRef();
}
왜일까..? 복사 생성자를 잘 보면 답을 알 수 있다.
missile->SetTarget(wraight);
위 함수가 호출되면, SetTarget 이 호출되기 위해 wraight 의 임시 객체가 생성된다. 즉, 아래 함수가 불린다!
// 복사
TSharedPtr(const TSharedPtr& rhs) { Set(rhs._ptr); }
rhs 가 만들어지고 나면, _ptr 은 자연스럽게 nullptr 가 되는데...
private:
T* _ptr = nullptr;
그 다음에, 바로 Set 함수가 불리게 된다.
private:
inline void Set(T* ptr)
{
_ptr = ptr;
if (ptr)
ptr->AddRef();
}
위의 Set 함수를 통해, rhs 로 맨 처음 들어왔던 ptr (즉, SetTarget 에서 타겟이었던 레이스) 은 Reference Count 가 하나 더 올라가게 된다.
void SetTarget(WraightRef target)
{
// target 의 _refCount 는 2 인 상태
_target = target;
// 중간에 개입하여 target 의 _refCount 를 1 빼더라도, 현재 함수에서 1을 더했기 때문에...
// target에 접근해도 crash 는 발생하지 않는다!
//target->AddRef();
}
따라서 refcount 와 관련된 크래시는 예방할 수 있다! 😉
아 물론, 이 경우에는 SetTarget 을 통해 RefCount 가 1 올라간 상태로 종료해 버리는 문제는 있지만...
지금은 SharedPtr 의 구현이 완벽한 것은 아니니, 나머지는 차후에 보충하도록 하겠다 😅
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 2-3. Allocator (0) | 2023.08.23 |
---|---|
[C++ 게임 서버] 2-2. 스마트 포인터 (0) | 2023.08.23 |
[C++ 게임 서버] 1-24. 연습문제 (소수의 갯수 구하기) (0) | 2023.08.08 |
[C++ 게임 서버] 1-23. DeadLock 탐지 (0) | 2023.08.08 |
[C++ 게임 서버] 1-22. Reader-Writer Lock (0) | 2023.08.07 |