KoreanFoodie's Study

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

Game Dev/Game Server

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

GoldGiver 2023. 12. 20. 06:25

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

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

핵심 :

1. Job 별로 클래스 상속을 무한히 늘려나가기 보다, Functor 와 Tuple 을 이용해 각 Job 을 간편하게 추가해 보자.

2. C++ 17 에서는 std::apply 를, C++ 11 에서는 Template Meta Programming 을 이용해 특정 Functor 에 임의의 갯수의 인자를 넘겨주는 기능을 구현할 수 있다. 

저번 시간에는 Job 과 JobQueue 에 대한 개념을 간단하게 소개했다.

그런데, 사실 Job 이 늘어날 때마다 이를 상속해서 클래스를 무한히 늘려나가는 방식은... Job 의 갯수가 늘어난다고 하면 매우 끔찍한 결과를 초래할 수 있다.

Job 을 추가하는 과정을 조금 간략화하기 위해, 템플릿을 이용해 각 Job 에 해당하는 동작을 Functor 로 저장할 수 있도록 만들면 어떨까? 🤔

 

C++ 17 에서 사용 가능한 아래 예제 코드를 보자.

// 함수자 (Functor)
class IJob
{
public:
	virtual void Execute() { }
};

template<typename Ret, typename... Args>
class FuncJob : public IJob
{
	using FuncType = Ret(*)(Args...);

public:
	FuncJob(FuncType func, Args... args) : _func(func), _tuple(args...)
	{

	}

	virtual void Execute() override
	{
		std::apply(_func, _tuple); // C++17
	}

private:
	FuncType _func;
	std::tuple<Args...> _tuple;
};

template<typename T, typename Ret, typename... Args>
class MemberJob : public IJob
{
	using FuncType = Ret(T::*)(Args...);

public:
	MemberJob(T* obj, FuncType func, Args... args) : _obj(obj), _func(func), _tuple(args...)
	{

	}

	virtual void Execute() override
	{
		std::apply(_func, _tuple); // C++17
	}

private:
	T*			_obj;
	FuncType	_func;
	std::tuple<Args...> _tuple;
};

먼저, Execute 를 호출할 IJob 인터페이스를 만들고, FuncJob 과 MemberJob 이 이를 상속받도록 한다.

FuncJob 부터 살펴보면... FuncType 의 Functor 와 tuple 을 받는 녀석인데, 이 때 Functor 는 해당 Job 의 실제 동작에 대한 함수를, tuple 은 해당 Functor 에 쓰일 인자를 의미한다.

std::apply 를 사용하면, 해당 Functor 에 tuple 을 대입해 실행 시킬 수 있는데, tuple 을 사용한 이유는... 임의의 갯수의 인자를 받아서 사용하고 싶은데, 멤버 변수로 Args... _args 같은 문법을 사용하는 것은 C++ 에서 지원하지 않기 때문이다! 🤣

MemberJob 은 그냥 클래스 멤버 함수를 Functor 로 사용할때 해당 클래스의 객체를 넣어서 사용할 것임을 파악할 수 있다.

 

위 방식을 사용하면, Room 클래스에서 PushJob 함수는 다음과 같이 간략화된다 :

template<typename T, typename Ret, typename... Args>
void PushJob(Ret(T::*memFunc)(Args...), Args... args)
{
    auto job = MakeShared<MemberJob<T, Ret, Args...>>(static_cast<T*>(this), memFunc, args...);
    _jobs.Push(job);
}

이제 job 은 Room 의 Member 함수를 받아  JobQueue 에 넣어줄 것이다.

 

이제 ClientPacketHandler 에서는, 아래와 같이 채팅 패킷을 Handling 하게 된다!

bool Handle_C_ENTER_GAME(PacketSessionRef& session, Protocol::C_ENTER_GAME& pkt)
{
	/** 기존과 동일 */
	
	//GRoom.PushJob(MakeShared<EnterJob>(GRoom, player));
	GRoom.PushJob(&Room::Enter, player);

	/** 기존과 동일 */
	return true;
}

bool Handle_C_CHAT(PacketSessionRef& session, Protocol::C_CHAT& pkt)
{
	/** 기존과 동일 */
	
	//GRoom.PushJob(MakeShared<BroadcastJob>(GRoom, sendBuffer));
	GRoom.PushJob(&Room::Broadcast, sendBuffer);

	/** 기존과 동일 */
	return true;
}

 

그럼 C++ 17 이전에는 어떻게 구현했을까? 이를 위해서는 템플릿 흑마법을 조금 살펴보아야 한다...

// C++11 apply
template<int... Remains>
struct seq
{};

template<int N, int... Remains>
struct gen_seq : gen_seq<N-1, N-1, Remains...>
{};

template<int... Remains>
struct gen_seq<0, Remains...> : seq<Remains...>
{};

template<typename Ret, typename... Args>
void xapply(Ret(*func)(Args...), std::tuple<Args...>& tup)
{
	xapply_helper(func, gen_seq<sizeof...(Args)>(), tup);
}

template<typename F, typename... Args, int... ls>
void xapply_helper(F func, seq<ls...>, std::tuple<Args...>& tup)
{
	(func)(std::get<ls>(tup)...);
}

// 함수자 (Functor)
class IJob
{
public:
	virtual void Execute() { }
};

template<typename Ret, typename... Args>
class FuncJob : public IJob
{
	using FuncType = Ret(*)(Args...);

public:
	FuncJob(FuncType func, Args... args) : _func(func), _tuple(args...)
	{

	}

	virtual void Execute() override
	{
		//std::apply(_func, _tuple); // C++17
		xapply(_func, _tuple);
	}

private:
	FuncType _func;
	std::tuple<Args...> _tuple;
};

음.. 사실 처음 봤을 때는 무슨 일이 일어나는지 전혀 감이 오질 않는다. 🤪

일단 xapply 부터 차근히 순서를 따라가보자. 이 녀석은 아래 부분을 호출하는데...

xapply_helper(obj, func, gen_seq<sizeof...(Args)>(), tup);

gen_seq<sizeof...(Args)> 는 Args 의 갯수 만큼을 받아 템플릿화한다. 즉, 인자(Args)로 들어온 것이 3개일 경우, gen_seq<3> 같은 식으로 뱉을 것이다.

그리고 나서 gen_seq 쪽을 보면, gen_seq 는 gen_seq<N-1, N-1, Remains...> 를 상속 받는데, 상속 구조를 정리해보면 아래와 같을 것이다 :

auto s = gen_seq<3>();
// gen_seq<3>
// : gen_seq<2, 2>
// : gen_seq<1, 1, 2>
// : gen_seq<0, 0, 1, 2>
// : seq<0, 1, 2>

위와 같이 계속 상속구조가 생기다가, 첫 인자가 0 이 되면 seq<Remains...> 를 상속받게 되면서 재귀가 종료될 것이다.

그럼 다시 xapply_helper 의 구현부를 보면, 

template<typename F, typename... Args, int... ls>
void xapply_helper(F func, seq<ls...>, std::tuple<Args...>& tup)
{
	(func)(std::get<ls>(tup)...);
}

seq 타입을 받는 것을 확인할 수 있다. 즉, func 는 seq<0, 1, 2> 를 시작으로 받으면서, std::get 을 통해 순서대로 <0>, <1>, <2> 를 인자로 받을 것임을 유추할 수 있다. 아래처럼 말이다.

auto tup = std::tuple<int32, int32>(1, 2);
auto val0 = std::get<0>(tup);
auto val1 = std::get<1>(tup);

 

멤버함수를 사용하는 경우에는, 다음과 같은 코드를 추가해 주면 될 것이다!

template<typename T, typename Ret, typename... Args>
void xapply(T* obj, Ret(T::*func)(Args...), std::tuple<Args...>& tup)
{
	xapply_helper(obj, func, gen_seq<sizeof...(Args)>(), tup);
}

template<typename T, typename F, typename... Args, int... ls>
void xapply_helper(T* obj, F func, seq<ls...>, std::tuple<Args...>& tup)
{
	(obj->*func)(std::get<ls>(tup)...);
}

template<typename T, typename Ret, typename... Args>
class MemberJob : public IJob
{
	using FuncType = Ret(T::*)(Args...);

public:
	MemberJob(T* obj, FuncType func, Args... args) : _obj(obj), _func(func), _tuple(args...)
	{

	}

	virtual void Execute() override
	{
		//std::apply(_func, _tuple); // C++17
		xapply(_obj, _func, _tuple);
	}

private:
	T*			_obj;
	FuncType	_func;
	std::tuple<Args...> _tuple;
};
Comments