KoreanFoodie's Study

[C++ 게임 서버] 6-4. JobQueue #3 본문

Game Dev/Game Server

[C++ 게임 서버] 6-4. JobQueue #3

GoldGiver 2023. 12. 20. 16:40

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 6-4. JobQueue #3

핵심 :

1. 이번 글에서는 JobSerializer 를 이용해, 람다 함수를 SharedPtr 로 만들어 Push 해 줌으로써 좀 더 간편하게 Job 관리하는 방법을 알아보자.

2. 람다와 SharedPtr 를 함께 쓴다고 해서 Memory Leak 이 일어나지는 않는다. 만약 문제가 있다면 람다와 SharedPtr 를 함께 사용해서 문제가 생기는 것이 아니라, 기본적인 설계가 잘못된 것이다. 

이번 시간에는 기존에 만들었던 Job 을 좀 더 간편하게 사용할 수 있도록 구조를 약간 수정해 보자. 먼저 Job 을 아래와 같이 ServerCore 에 만들어줄 것이다.

/*---------
	Job
----------*/

using CallbackType = std::function<void()>;

class Job
{
public:
	Job(CallbackType&& callback) : _callback(std::move(callback))
	{
	}

	template<typename T, typename Ret, typename... Args>
	Job(shared_ptr<T> owner, Ret(T::* memFunc)(Args...), Args&&... args)
	{
		_callback = [owner, memFunc, args...]()
		{
			(owner.get()->*memFunc)(args...);
		};
	}

	void Execute()
	{
		_callback();
	}

private:
	CallbackType _callback;
};

잘 보면, Job 은 그냥 Callback 함수를 하나 받을 수도 있고, 특정 클래스 객체의 멤버함수를 받을 수도 있음을 알 수 있다.

Execute 를 하면, 콜백함수가 불리게 된다.

 

JobQueue 클래스는 아래와 같이 생겼다 :

class JobQueue
{
public:
	void Push(JobRef job)
	{
		WRITE_LOCK;
		_jobs.push(job);
	}

	JobRef Pop()
	{
		WRITE_LOCK;
		if (_jobs.empty())
			return nullptr;

		JobRef ret = _jobs.front();
		_jobs.pop();
		return ret;
	}

private:
	USE_LOCK;
	queue<JobRef> _jobs;
};

 

사실 이번 글의 핵심은 아래 소개할 JobSerializer 이다.

JobSerializer 는, 이전에 우리가 PushJob 을 했던 부분을 좀더 API 화 시킨 느낌이다. 코드를 보면...

/*-----------------
	JobSerializer
------------------*/

class JobSerializer : public enable_shared_from_this<JobSerializer>
{
public:
	void PushJob(CallbackType&& callback)
	{
		auto job = ObjectPool<Job>::MakeShared(std::move(callback));
		_jobQueue.Push(job);
	}

	template<typename T, typename Ret, typename... Args>
	void PushJob(Ret(T::*memFunc)(Args...), Args... args)
	{
		shared_ptr<T> owner = static_pointer_cast<T>(shared_from_this());
		auto job = ObjectPool<Job>::MakeShared(owner, memFunc, std::forward<Args>(args)...);
		_jobQueue.Push(job);
	}

	virtual void FlushJob() abstract;

protected:
	JobQueue _jobQueue;
};

Job 객체를, ObjectPool 을 이용해 SharedPtr 형태로 JobQueue 에 넣어 주고 있음을 확인할 수 있다.

뭔가 신기하다고 느껴질 수 있는 부분인데... 람다 함수를 SharedPtr 로 넣어줌으로써 Dangling Pointer 이슈 등을 해결하고 있다.

FlushJob 의 경우, 이전에 Room 클래스에서 했던 것처럼 while 문을 돌면서 JobQueue 의 Job 을 하나씩 꺼내 Execute 를 호출해 줄 것이다. 😉

그럼 게임 서버는 ClientPacketHandler 에서 아래와 같이 Job 을 간편하게 Push 해주게 된다! 😊

bool Handle_C_CHAT(PacketSessionRef& session, Protocol::C_CHAT& pkt)
{
	std::cout << pkt.msg() << endl;

	Protocol::S_CHAT chatPkt;
	chatPkt.set_msg(pkt.msg());
	auto sendBuffer = ClientPacketHandler::MakeSendBuffer(chatPkt);

	GRoom->PushJob(&Room::Broadcast, sendBuffer);

	return true;
}

 

참, 검색을 하다 보면 람다와 SharedPtr 를 같이 쓰면 Memory Leak 이 발생한다는 그런 얘기가 떠도는데... 이는 사실이 아니다!

뭐.. 만약에 다음과 같은 클래스는 순환 참조가 발생해 Memory Leak 이 발생할 것이다 :

class Knight : public enable_shared_from_this<Knight>
{
public:
	Knight() : _knight(shared_from_this())
	{
	}

protected:
	shared_ptr<Knight> _knight;
};

만약 위와 같은 코드가 있다면... 참조가 해제가 안되어 Memory Leak 이 발생할 것이다. 😅

람다와 SharedPtr 를 같이 사용한다고 memory leak 이 발생하는 것은 아니니, 문제가 있다면 설계가 이상하지 않은지를 되짚어보자! 😁

Comments