이전 강의까지 Job을 만드는 여러가지 방법을 알아봤다.
직접 class 만들기, functor만들기, 람다함수 이용하기
조심해야할 것은 shared_ptrf를 쓸 때 메모리 릭을 조심해야 하는 것.
아래와 같은 코드도 owner(자기 자신)을 job에 넣고 job을 자기가 들고있는 jobqueue에 넣음으로 자기 자신의 쉐어드 포인터를 들고있어서 메모리 릭이 발생한다.
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);
}
shared_ptr 이 아닌 weak_ptr로 들고있는다던지, 객체가 필요없어져서 삭제가 일어나야할 때 jobQueue를 clear한다던지 해서 해결할 수 있다.
지금은 job을 넣는애 따로, 실행하는 애 따로이다.
근데 프로젝트에 따라 job을 실행하는 애들이 매우 매우 많아질 수 있다. 심리스MMO를 만드는 경우가 그렇다고 한다 (검은사막 같은 게임). 만약 지금같은 방식이라면 flush 함수를 호출하는게 수십만개가 될 수 있는데 그것들을 0번부터 50만번까지 순회하면서 일감이 있는지 확인하고, flush함수를 호출한다? 이건 말도 안된다. condition_variable을 이용해 필요 없을때는 대기하고 일감이 생겼을 때만 동작하게 할 수 있지만 그것도 한두개지 객체가 많아지면 말이 안된다고 한다.
강의자가 가장 좋아하는 스타일은 pushjob을 할 때 걔가 실행까지 담당하게 하는것을 선호한다고 한다. (지금 Session::Send처럼. Send가 어떻게 동작하더라?)
그래서 오늘 해볼것은 그런식으로 바꿔보도록 하겠다.
파일
JobQueue -> LockQueue로 이름 변경
JobSerializer -> JobQueue로 이름 변경
지금까지는 FlushJob을 추상함수로 만들어서 JobQueue를 상속받은 애들이알아서 만들도록 했는데, 이제 PushJob을 할 때 job을 만들어가지고 경우에 따라서 내가 처음으로 일감을 넣은 애라고 하면 실행까지 담당하도록 코드를 수정해보자.
class JobQueue : public enable_shared_from_this<JobQueue>
{
public:
void DoAsync(CallbackType&& callback)
{
Push(ObjectPool<Job>::MakeShared(std::move(callback)));
}
template<typename T, typename Ret, typename... Args>
void DoAsync(Ret(T::* memFunc)(Args...), Args... args)
{
shared_ptr<T> owner = static_pointer_cast<T>(shared_from_this());
Push(ObjectPool<Job>::MakeShared(owner, memFunc, std::forward<Args>(args)...));
}
void ClearJobs() { _jobs.Clear(); }
private:
void Push(JobRef&& job);
void Execute();
protected:
LockQueue<JobRef> _jobs;
Atomic<int32> _jobCount = 0;
};
void JobQueue::Push(JobRef&& job)
{
const int32 prevCount = _jobCount.fetch_add(1);
_jobs.Push(job);
if (prevCount == 0)
{
Execute();
}
}
void JobQueue::Execute()
{
while (true)
{
Vector<JobRef> jobs;
_jobs.PopAll(OUT jobs);
const int32 jobCount = static_cast<int32>(jobs.size());
for (int32 i = 0; i < jobCount; i++)
jobs[i]->Execute();
//남은 일감이 0개라면 종료
if (_jobCount.fetch_sub(jobCount))
{
return;
}
}
}
Push를 했을 때 해당 쓰레드가 첫번째로 Push를 했다면 그 쓰레드가 JobQueue가 빌때까지 Execute를 실행하는 코드.
완벽한건 아니고 생각할 부분이 있는게 만약 몇천명의 유저와 수십만개의 몬스터가 동작한다 하면, 일감이 너무 몰리면 해당 쓰레드가 execute에서 벗어나질 못하는 문제가 생길 수 있다. 그런데 MMO라면 렉이 걸리면 모두가 공평하게 렉이 걸려야지 한명은 잘 움직이는데 나머지는 3초동안 멈춰있고 그러면 불공평할 것이다.
또 DoAsync를 타고 타고 가서 절대 끝나지 않은 상황이 발생할 수 있다. (일감이 한 쓰레드에 몰릴 때)
만약 어떤 이유로 Execute를 실행했더니 그 안에서도 DoAsync를 넣었다고 하자. 근데 마침또 이 작업의 1번째라면 해당 jobqueue도 이 쓰레드가 처리해야 하는 상황이 될것이다. 거기에 또 다른것에 대한 DoAsync도 이어지면 끝도없이 계속 execute만 하면서 처음 처리하려 했던 일을 포함해서 제대로 일처리를 못하게 되는 것.
그러니 이왕이면 일감의 배분이 적당히 될 수 있게끔 만들어 주는게 중요할것.
'강의 수강 > 게임서버(1)' 카테고리의 다른 글
| 75. JobTimer (0) | 2025.08.25 |
|---|---|
| 74. JobQueue #5 (0) | 2025.08.25 |
| 강의를 듣고 다시 확인할 것들 모음 (0) | 2025.08.23 |
| 72. JobQueue #3 (0) | 2025.08.22 |
| 71. JobQueue #2 (0) | 2025.08.19 |