지금까지 열심히 멀티스레딩 관련 공부를 진행했다. 이젠 메모리에 대한 정리를 진행하겠다. C++의 경우 다른 언어와 달리 직접적으로 메모리를 관리를 해줘야 하는 언어이기 때문에, 메모리에 대한 정책 또한 제대로 알고 갈 필요가 있다.
예로 들어 스타크래프트와 같은 게임을 개발한다고 가정하면 다음과 같이 코드를 짤 것이다.
class Wraight
{
public :
int _hp = 150;
int _posX = 150;
int _posY = 150;
};
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);
while(true)
{
if(missile)
{
missile->Update();
}
}
}
이 때 보통 new를 했다면 delete를 할 것이다. 문제는 사용하기 전에 delete를 해버리게 되는 것이다. 이런 경우 크래시가 나면 오히려 문제가 나지 않는다. 진짜 문제는 delete를 했을 때 접근하면 안 될 메모리에 접근해버리는 경우다. (보통 0xcccccccc일 것이다.) 즉 메모리를 오염시킨다는 것이다.
문제는 MMORPG 개발 도중에 골드를 관리하는 메모리인데 이를 건들이게 되면 굉장히 큰일이 난다는 것이다. 이 말은 메모리를 관리하고 만약 허락된 메모리 외적으로 건들이기 않게 하는 것이 중요하다는 것이다.
먼저 메모리를 생성했을 경우 나를 참조하는 객체가 없어지는 경우 객체를 자동으로 delete할 수 있게 만들어보자. 이를 위해서 프로그래밍 언어론에서 제공되는 개념 중 하나인 참조 카운트(reference count)란 물건이 있다.
namespace FrokEngine
{
class RefCountable
{
public :
RefCountable() : _refCount(1) {}
virtual ~RefCountable() {}
int32 GetRefCount() { return _refCount; }
int32 AddCount() { return ++_refCount; }
int32 ReleaseRef()
{
int32 refCount = --refCount;
if (refCount == 0)
{
delete this;
}
return refCount;
}
protected :
int32 _refCount;
};
}
그리고 기존의 객체들은 다음과 같이 선언이 된다.
class Wraight : class RefCountable;
class Missile : class RefCountable;
그리고 만약 다음과 같은 객체를 사용하면 다음과 같이 사용을 해야 한다.
void SetTarget(Wraight* target)
{
_target = target;
_target->AddRef();
}
그리고 만약 사용이 끝나면 사용을 안 하겠다는 뜻이니, 다음과 같이 레퍼런스 카운터를 1 줄여줘야 한다.
void Update()
{
if(_target == nullptr) return;
int posX = target->_posX;
int posY = target->_posY;
// TODO : 쫒아가기
if(target->_hp == 0)
{
_target->ReleaseRef();
_target = nullptr;
}
}
즉 이젠 우리가 직접 delete를 하는 것이 아니라 refCount가 0이 되면 자동으로 delete가 되게 만드는 식으로 만들어줘야 한다는 것이다. 즉 사용한 뒤 만약 refCount를 체크하고 0이 된다면 nullptr로 밀어주고 있기 때문에, delete 이후 쓰레기값을 수정할 일이 없다는 것이다.
이 개념은 굉장히 중요한 개념이다. 하지만 지금까지 코드는 refCount를 수동으로 관리하고 있는데 만약 내가 Add나 Release를 하지 않았다면 사실상 쓰나마나 한 문제가 발생한다.
그리고 싱글 스레드에는 문제가 없지만, 멀티스레딩에서는 문제가 발생하며 만약 멀티스레딩에서 돌아간다면 중간에 다른 스레드의 개입으로 인해 refCount가 엉망진창이 될 수 있다는 점이다. 이는 컨텐츠 단에서도 마찬가지다. 만약 내가 Add를 하려는데 이미 그 전에 다른 스레드가 Release를 해서 delete가 된 상태라면 굉장히 골치아파진다. 즉 자동적으로 할 수 있게 만드는 장치가 필요하다. 즉 멀티스레딩이 보장된 스마트 포인터를 만드는 편이 좋아보인다.
먼저 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 IsNull() { return _ptr == nullptr; }
// 다양한 상황에 대한 operator mapping
bool operator==(const TSharedPtr& rhs) const
{
_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;
}
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;
};
즉 내가 직접 Add, Release를 하는 것이 아니라 포인터에서 대신 관리해준다. 그리고 실제 shared_ptr에 가깝게 디자인을 했기 때문에 실제 포인터처럼 사용할 수 있다.
using WraightRef = TSharedPtr<Wraight>;
using MissileRef = TSharedPtr<Missile>;
int main()
{
WraightRef wraight(new Wraight());
MissileRef missile(new Missile());
만약 내가 필요없다면 nullptr로 밀어주기만 하면 된다.
wraight = nullptr;
이 코드는 다음과 같은 효과를 가진다.
wraight = WraightRef(nullptr);
이유는 복사 생성자를 통해서 만약 nullptr로 받아준다면 이는 Release를 밀어준 뒤 Set(nullptr)이 되며 레퍼런스 카운터가 줄어드는 방식이다.이제 refCount를 확인해보자.
시작은 new Wraight를 통해서 생성되는 refCount 1 그리고 wraight 객체가 생성되고 획득되면서 늘어나는 refCount 1 총 2개를 가지고 시작한다. 하지만 new Wraight 객체는 우리가 직접 관리하는 것이 아닌 wraight 객체를 이용해서 관리를 해줄 것이기 때문에 ReleaseRef를 해준다.
이후 missile이 wraight를 참조하면 _refCount가 1 증가한 것을 확인할 수 있다.이제 이런 경우 missile이 target을 관리해줄 것이기 때문에 wraight에 nullptr을 넣어 refCount를 줄여준다.
이후 열심히 미사일로 공격을 진행한다. 만약 _target이 되는 wraight가 체력이 0이 된다면 다음과 같이 _refCount를 줄여주게 될 것이다.
이 때 _refCount가 0이 되면 nullptr로 밀어주게 된다.
지금까지 한 작업은 실제 C++에서 제공하는 스마트 포인터인 SharedPtr에서도 구현이 되어있는 부분이다.
아쉬운 부분은 메서드를 타고 타다보면 결국 이 방식의 포인터는 어느 정도 자원을 계속 소모할 수 밖에 없으며(refCount를 고려해야 하다보니), 지금은 RefCountable을 만들어서 사용을 했지만 외부 라이브러리의 경우 이런 것을 붙여가며 사용할 수 없기 때문에 이런 클래스를 최상위 객체로 사용하는 경우는 거의 없다.
'Game Server' 카테고리의 다른 글
[게임서버/C++] Base Memory Allocating (0) | 2022.08.22 |
---|---|
[C++/게임서버] 스마트 포인터 (0) | 2022.08.22 |
[C++/게임서버] Deadlock Profiler (0) | 2022.07.29 |
[C++/게임서버] Reader-Writer Lock (0) | 2022.07.25 |
[C++/게임서버] ThreadManager (0) | 2022.07.21 |