| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
Tags
- 강한 연결 요소
- 비트마스킹
- Prisma
- 2-SAT
- Express.js
- 벨만-포드
- Github
- localstorage
- Delete
- DP
- 비트필드를 이용한 dp
- trie
- map
- Binary Lifting
- Behavior Design Pattern
- JavaScript
- SCC
- R 그래프
- reference counting
- 트라이
- ccw 알고리즘
- Strongly Connected Component
- Lock-free Stack
- 그래프 탐색
- 이분 탐색
- PROJECT
- Spin Lock
- 최소 공통 조상
- 게임 서버 아키텍처
- 자바스크립트
Archives
- Today
- Total
dh_0e
[C++/Game Server] Reference Counting (in Shared Pointer) 본문
GameServer.cpp
class Wraith
{
public:
int _hp = 150;
int _posX = 0;
int _posY = 0;
private:
};
class Missile
{
public:
void SetTarget(Wraith* target)
{
_target = target;
}
void Update()
{
int posX = _target->_posX;
int posY = _target->_posY;
// TODO : 타겟을 쫓아감
}
private:
Wraith* _target = nullptr;
};
int main()
{
Wraith* wraith = new Wraith();
Missile* missile = new Missile();
missile->SetTarget(wraith);
// 레이스 피격시
wraith->_hp = 0;
delete wraith;
while (true)
{
if (missile)
{
missile->Update();
}
}
return 0;
}
- 다음과 같은 상황에서 missile 클래스가 이미 delete 된 메모리(Dangling Pointer)를 참조하고 있어서 CRASH는 나지 않지만, 엉뚱한 코드를 계속 추출하며 오염된 메모리에 접근 중
- 참조 카운트(Reference Counting)를 만들어 해당 메모리를 참조하는 객체가 있는지 확인한 뒤, delete를 해줘야 함
RefCounting.h
#pragma once
#include "pch.h"
/*----------------
RefCountable
-----------------*/
class RefCountable {
public:
RefCountable() : _refCount(1) {}
virtual ~RefCountable() {}
// memory leak(메모리 누수)를 예방하기 위해 항상 최상위 클래스에 virtual을 붙임
int32 GetRefCount() { return _refCount; }
int32 AddRef() { return ++_refCount; }
int32 ReleaseRef() {
int32 refCount = --_refCount;
if (refCount == 0) {
delete this;
}
return refCount;
}
protected:
atomic<int32> _refCount;
};
/*--------------------
SharedPtr
--------------------*/
template <typename T>
// 포인터의 ref count를 관리하는 스마트 포인터
class TSharedPtr {
public:
TSharedPtr() {}
TSharedPtr(T* ptr) { Set(ptr); }
// 복사
TSharedPtr(const TSharedPtr& other) { Set(other._ptr); }
// 이동
TSharedPtr(TSharedPtr&& other) {
_ptr = other._ptr;
other._ptr = nullptr;
}
// 상속 관계 복사자
template <typename U>
TSharedPtr(const TSharedPtr<U>& other) {
Set(static_cast<T*>(other._ptr));
}
// 만약 Knight 클래스가 Player 클래스를 상속받았다면,
// TSharedPtr<Knight>를 TSharedPtr<Player>에 넣을 수 있어야함
// 이 템플릿 생성자가 그 형변환을 가능하게 해줌
// 소멸
~TSharedPtr() { Release(); }
public:
// 복사 연산자
TSharedPtr& operator=(const TSharedPtr& other) {
if (_ptr != other._ptr) {
Release();
Set(other._ptr);
}
return *this;
}
// 이동 연산자
TSharedPtr& operator=(TSharedPtr&& other) {
Release();
_ptr = other._ptr;
other._ptr = nullptr;
return *this;
}
bool operator==(const TSharedPtr& other) const { return _ptr == other._ptr; }
bool operator==(T* ptr) { return _ptr == ptr; }
bool operator!=(const TSharedPtr& other) const { return _ptr != other._ptr; }
bool operator!=(T* ptr) const { return _ptr != ptr; }
bool operator<(const TSharedPtr& other) const { return _ptr < other._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; }
// get 대신 연산자 오버로딩을 통해 TSharedPtr 클래스를 마치 포인터처럼 사용할 수 있게 함
// 오버로딩 전: ptr.GetPtr()->hp = 100;
// 오버로딩 후: ptr->hp = 100;
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;
};
- 그냥 Reference Count(RefCountable)만 구현하여 사용해도 되지만 멀티 스레드 환경에선 여러 변수가 발생
- ex) 아래 GameServer.cpp의 A 부분
- Shared Pointer(TSharedPtr)를 구현하여 Reference Count를 기반으로 자동으로 메모리를 정리하는 스마트 포인터를 구현
GameServer.cpp
#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <atomic>
#include <mutex>
#include <Windows.h>
#include <future>
#include "ThreadManager.h"
#include "RefCounting.h"
class Wraith : public RefCountable {
public:
int _hp = 150;
int _posX = 0;
int _posY = 0;
};
using WraithRef = TSharedPtr<Wraith>;
class Missile : public RefCountable {
public:
void SetTarget(WraithRef target) {
_target = target;
// A) 멀티 스레드 환경에서 이 부분에 다른 스레드의 개입이 가능함
// 아래의 AddRef()를 하기 전에 다른 스레드에서 Release를 통해 target을 nullptr로 만들 수 있음
// Shared Pointer를 구현한 이유
// _target->AddRef();
Test(target);
}
void Test(WraithRef& target) {
// target을 다른 함수에서 잠깐 사용한다고 가정
// 이 경우엔 참조로 받아와야 쓸데없는 시간 낭비가 발생하지 않음
// WraithRef를 복사하는 식으로 받아오면 atomic연산을 쓸데없이 2번 해야하기 때문
}
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;
}
private:
WraithRef _target = nullptr;
};
using MissileRef = TSharedPtr<Missile>;
int main() {
WraithRef wraith(new Wraith());
wraith->ReleaseRef();
// Wraith를 만들 때 ref count를 1로 초기화, TSharedPtr에서 Set()을 호출하기 때문에 ref count가 2가 되기 때문에,
// ReleaseRef()를 호출하여 ref count를 1로 초기화
MissileRef missile(new Missile());
missile->ReleaseRef();
missile->SetTarget(wraith);
// 레이스 피격시
wraith->_hp = 0;
// delete wraith;
// wraith->ReleaseRef();
wraith = nullptr;
while (true) {
if (missile) {
if (missile->Update()) {
missile->ReleaseRef();
missile = nullptr;
}
}
}
// missile->ReleaseRef();
missile = nullptr;
return 0;
}
- Wraith, Missile 클래스를 모두 Shared Pointer로 사용하여 참조 포인터 생성 시 자동으로 Ref count 증가, nullptr 복사 시 Set에서 자연스럽게 Ref count가 감소 및 nullptr로 초기화
- Ref count가 0이 되면 자동으로 메모리 삭제
Shared Pointer의 한계
- RefCountable을 상속 받아야만 사용이 가능하기 때문에, 이미 만들어진 클래스를 대상으로 사용이 불가능함
- 순환(Cycle) 문제
- 아래와 같이 서로 참조를 하게 되는 상황들에서 순환 문제가 발생
- 데드락 문제처럼 서로를 참조하며 안 놔줌
- Weak Pointer를 사용하여 해결할 수 있음
using KnightRef = TSharedPtr<class Knight>;
class Knight : public RefCountable {
public:
Knight() { cout << "Knight()" << endl; }
~Knight() { cout << "~Knight()" << endl; }
void SetTarget(KnightRef target) { _target = target; }
private:
KnightRef _target = nullptr;
};
int main() {
KnightRef k1(new Knight());
k1->ReleaseRef();
KnightRef k2(new Knight());
k2->ReleaseRef();
k1->SetTarget(k2);
k2->SetTarget(k1);
k1 = nullptr;
k2 = nullptr;
return 0;
}
using KnightRef = TSharedPtr<class Knight>;
using InventoryRef = TSharedPtr<class Inventory>;
class Knight : public RefCountable {
public:
Knight() { cout << "Knight()" << endl; }
~Knight() { cout << "~Knight()" << endl; }
void SetTarget(KnightRef target) { _target = target; }
KnightRef _target = nullptr;
InventoryRef _inventory = nullptr;
};
class Inventory : public RefCountable {
public:
Inventory(KnightRef knight) : _knight(knight) {}
KnightRef _knight = nullptr;
// KnightRef를 참조하면 Ref Count를 증가시켜 순환 참조 발생
// weak_ptr<Knight> _knight;
// weak_ptr로 Knight 정보를 받아오면 Ref Count를 증가시키지 않아 순환 참조가 발생하지 않음
};
int main() {
KnightRef k1(new Knight());
k1->ReleaseRef();
k1->_inventory = new Inventory(k1);
return 0;
}
참고) std::move의 정체: "강제 형변환"
- std::move(ptr1)를 실행하면 내부적으로는 다음과 같은 일이 일어남
- ptr1은 원래 이름이 있는 멀쩡한 변수(L-value)임
- std::move가 얘를 TSharedPtr&& 타입으로 강제로 바꿔버림
- 대입 연산자(=)를 만났을 때, 컴파일러는 두 가지 선택지 중 고민
- operator=(const TSharedPtr&) (복사)
- operator=(TSharedPtr&&) (이동)
- 방금 && 타입으로 변신했으니, 당연히 이동 연산자(&&) 쪽 연산자 오퍼레이터 함수를 사용
'C++ > Game Server' 카테고리의 다른 글
| [C++/Game Server] Stomp Allocator (Use-after-free & Overflow 오류 검증) (1) | 2026.03.10 |
|---|---|
| [C++/Game Server] Allocator (New, Delete Operator Overloading) + std::forward, Placement New (0) | 2026.03.10 |
| [C++/Game Server] DeadLock Profiler (+ release, debug 모드) (0) | 2026.02.19 |
| [C++/Game Server] Reader-Writer Lock (0) | 2026.02.12 |
| [C++/Game Server] Thread Manager (+각 서버 file 설명) (0) | 2026.02.11 |
