나만의 작은 도서관
[TIL][C++] 250421 MMO 서버 개발 1일차: jobqueue를 구현하는 방법 세 가지 본문
주의사항: 해당 글은 다듬지 않은 날것 그대로인 글입니다.
jobqueue를 구현하는 첫번째 방법#1: 각 job을 클래스로 정의
- job 인터페이스인 IJOb을 만든 다음, 해당 인터페이스를 상속받아 job을 정의한다.
- 그리고 각 job을 IJob타입으로 업캐스팅하여 jobqueue에 저장한다.
- 소비자 역할을 맡은 쓰레드는 jobqueue에서 job을 하나씩 빼서 처리한다.
장단점
- 코드가 굉장히 직관적이다.
- 새로운 job이 생기면 이에 따른 job을 새롭게 정의해야한다. job의 종류가 많아지면 굉장히 코드가 길어진다. ⇒ 코드량 2~3배 증가
jobqueue를 구현하는 두번째 방법#2: 각 job을 functor로 정의
- functor 방식으로 작업에 해당하는 함수를 저장한다.
- 템플릿+가변 인자를 이용하면 임의의 리턴 타입 및 매개변수 리스트를 가진 함수 포인터를 받을 수 있다.
- 인자는 tuple을 이용해 functor에 넘겨주고, apply()를 통해 tuple의 인자를 함수에 적용하여 호출한다.
// Args... _args; <- 이런 문법은 아쉽게도 없음
std::tuple<Args...> _tuple;
// 멤버 함수 방식
Ret Execute()
{
std::apply(_func, _tuple);
}
- 멤버 함수를 저장할 수도 있다.
template<typename T, typename Ret, typename... Args>
class MemberJob
{
using FuncType = Ret(T::*)(Args...); // 멤버 함수 포인터
public:
MemberJob(T* obj, FuncType func, Args... args) : _obj(obj), _func(func), _tuple(args) {}
Ret Execute()
{
std::apply(_func, _tuple) // C++17
}
private:
T* _obj;
FuncType _func;
std::tuple<Args...> _tuple;
}
장단점
- 장점: 종류에 따른 각 Job을 만들지 않아도 된다!.
jobqueue를 구현하는 세번째 방법#3: 람다 활용하기
PlayerRef player = make_shared<Player>();
std::function<void(void)> func = [=]() // GRoom::Enter를 실행하는 functor 생성
{
GRoom.Enter(player);
};
- functor를 직접 만드는 대신, 람다로 functor를 만든다.
- 멤버 변수를 람다 캡쳐로 캡쳐시, this가 캡쳐되는 것을 조심.
- 되도록이면 캡쳐 대상을 명시
shared_ptr<Room> GRoom;
PlayerRef player = make_shared<Player>();
// Room의 shared_ptr을 복사해 람다가 가지고 있음으로써, 작업을 실행할때까지
// Room이 유지되는 것을 보장.
std::function<void()> func = [self = GRoom, &player]()
{
self->Enter(player);
}
최종 코드 JobSerializer: jobQueue에 대한 공통 기능 클래스. JobQueue를 사용하고자 하는 클래스가 이를 상속한다.
- 함수 포인터, 람다로 받아 function<void(void)>에 넣어서 jobQueue에 push.
// FlushJob에 의해 추상 클래스 취급.
class JobSerializer : public enable_shared_from_this<JobSerializer>
{
public:
void PushJob(CallbackType&& callback)
{
_jobQueue.Push(make_shared<Job>(std::move(callback)));
}
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());
_jobQueue.Push(make_shared<Job>(owner, memFunc, std::forward<Args>(args)...));
}
virtual void FlushJob() = 0; // 순수 가상 함수.
protected:
JobQueue _jobQueue;
}
장단점
- 장점: 두번째 방법보다 쉽고 간결하게 functor를 활용할 수 있다.
- 람다, 멤버 함수 두 가지 방식으로 job을 push할 수 있다.