객체를 생성시키고 소멸시킬 때 문제점이 있다. A라는 객체가 B라는 객체를 참조하고 있는 경우 B라는 객체가 용도를 다해서 소멸시키게 되면 A는 이상한 메모리 공간을 참조하게 되어버린다. 이를 해결하기 위해 Reference Counting을 하는데 자신을 참조하고 있는 수를 센 후 0이 되었을 때 소멸시키는 것이다. 코드를 보면 더욱 이해하기 쉬울 것이다.
RefCounting.h
#pragma once
//-----------------
// RefCountable
//-----------------
class RefCountable
{
public:
RefCountable() : _refCount(1) {}
virtual ~RefCountable() {} // 최상위 클래스의 소멸자에는 virtual(메모리 릭 예방)
int32 GetRefCount() { return _refCount; }
int32 AddRefCount() { return ++_refCount; }
int32 ReleaseRef()
{
int32 refCount = --_refCount;
if (refCount == 0)
{
delete this;
}
return refCount;
}
protected:
atomic<int32> _refCount;
};
객체가 생성되면 1개는 참조를 하고 있으므로 생성자에서 _refCount를 초기화 해준다.
그럼 어떻게 사용되는지 간단한 예시로 살펴보자
Main.cpp
#include "pch.h"
#include "CorePch.h"
#include <Windows.h>
#include "ThreadManager.h"
#include "RefCounting.h"
class Wraith : public RefCountable
{
public:
int _hp = 100;
int _posX = 0;
int _posY = 0;
};
class Missile : public RefCountable
{
public:
void SetTarget(Wraith* target)
{
_target = target;
target->AddRefCount();
}
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;
}
Wraith* _target = nullptr;
};
int main()
{
Wraith* wraith = new Wraith();
Missile* missile = new Missile();
missile->SetTarget(wraith);
wraith->_hp = 0;
wraith->ReleaseRef();
wraith = nullptr;
//delete wraith;
while (true)
{
if (missile)
{
if (missile->Update())
{
missile->ReleaseRef();
missile = nullptr;
}
}
}
missile->ReleaseRef();
missile = nullptr;
//delete missile;
}
미사일이 레이스를 추적하는 것을 간단하게 나타내 보았다. RefCountable 클래스를 상속받아 사용하고 delete를 해야할 곳에 단순히 ReleaseRef만 해주면 된다. 이후 null로 초기화를 해준다. 하지만 멀티스레드에서는 또 다르다. 객체를 참조하고 AddRefCount를 하는 것이 원자적으로 일어나지 않기 때문에 그 사이에 객체가 소멸될 수도 있다.
이를 해결하기 위해 스마트포인터 형태로 묶어 사용하는 방법을 알아보자.
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; }
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을 구현한 모습이다.
Main.cpp
#include "pch.h"
#include "CorePch.h"
#include <Windows.h>
#include "ThreadManager.h"
#include "RefCounting.h"
class Wraith : public RefCountable
{
public:
int _hp = 100;
int _posX = 0;
int _posY = 0;
};
using WraithRef = TSharedPtr<Wraith>;
class Missile : public RefCountable
{
public:
void SetTarget(WraithRef target)
{
_target = target;
//target->AddRefCount();
}
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;
}
WraithRef _target = nullptr;
};
using MissileRef = TSharedPtr<Missile>;
int main()
{
WraithRef wraith(new Wraith()); // refCount가 2가 됨
wraith->ReleaseRef();
MissileRef missile(new Missile());
missile->ReleaseRef();
missile->SetTarget(wraith);
wraith->_hp = 0;
//wraith->ReleaseRef();
wraith = nullptr;
//delete wraith;
while (true)
{
if (missile)
{
if (missile->Update())
{
//missile->ReleaseRef();
missile = nullptr;
}
}
}
//missile->ReleaseRef();
missile = nullptr;
//delete missile;
}
이제 처음 생성할 때만 ReleaseRef를 1번 해주면 이후 main에서 직접 ReleaseRef를 하지 않아도 스마트포인터가 알아서 처리해준다. 연산자 오버로딩을 해주었기 때문에 일반 포인터와 똑같이 사용할 수 있다. 결국 SetTarget에서 객체가 소멸될 가능성이 있기 때문에 처음의 코드가 문제가 있었지만 위의 코드는 SetTarget에 진입하는 시점에 카운트가 최소 1이상이 되어 안정적으로 작동이 된다.
중요한 점은 Set의 과정에서 객체가 소멸될 일이 없다는 것이다!!
하지만 이렇게 직접 구현한 포인터는 단점이 존재한다.
1) 상속을 받아야만 사용할 수 있기 때문에 이미 만들어진 클래스 대상으로 사용이 불가능하다.
2) 표준 shared pointer도 가지고 있는 문제로 순환(cycle) 문제가 있다. 예를 들면 1 vs 1의 전투라고 가정했을 때 A가 B를 타겟으로 하고(참조) B가 A를 타겟으로 하게 되면 서로 참조를 하는 상황이 발생한다. 그렇게 되면 두 객체가 소멸될 수 없게 된다. 어떻게 보면 데드락과 유사한 상황이라고도 볼 수 있다.
순환 문제는 실무에서도 빈번하게 일어나는 문제라고 한다. 그렇기 때문에 제대로 이해하고 사용하는 것이 좋을 것 같다.
처음 공부하는 부분이라 그런지 꽤나 많이 어렵다. 하지만 면접에서도 스마트포인터에 관한 내용이 자주 등장하니 숙지하도록 하자.
'서버 > 메모리 관리' 카테고리의 다른 글
6. Memory Pool #1 (0) | 2024.08.20 |
---|---|
5. STL Allocator (0) | 2024.08.16 |
4. Stomp Allocator (0) | 2024.08.16 |
3. Allocator (0) | 2024.08.11 |
2. 스마트포인터 (0) | 2024.08.11 |