이전 강의까지 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

+ Recent posts