KoreanFoodie's Study
[C++ 게임 서버] 6-7. JobTimer 본문
[C++ 게임 서버] 6-7. JobTimer
핵심 :
1. JobTimer 를 사용하면, Job 의 배분을 일정 주기 이후 실행하도록 균등하게 배분할 수 있다.
2. 세션이 종료될 때 Memory Leak 이 일어나지 않도록 종료 처리를 잘 해주자.
이전에 Job 의 처리를 쓰레드에게 어느 정도 균등하게 분배하기 위해 Tick 과 소유권에 대한 개념을 도입했다.
그런데 각 쓰레드 별로 Tick 을 루프를 돌며 체크하는 일은 엄청난 낭비가 아닐 수 없다. 😅 이 문제를 해결하기 위해, Global 하게 JobTimer 라는 녀석을 도입해 일정 시간 후, Job 을 알아서 처리하도록 만들어 보자.
JobTimer.h
struct JobData
{
JobData(weak_ptr<JobQueue> owner, JobRef job) : owner(owner), job(job)
{
}
weak_ptr<JobQueue> owner;
JobRef job;
};
struct TimerItem
{
bool operator<(const TimerItem& other) const
{
return executeTick > other.executeTick;
}
uint64 executeTick = 0;
JobData* jobData = nullptr;
};
/*--------------
JobTimer
---------------*/
class JobTimer
{
public:
void Reserve(uint64 tickAfter, weak_ptr<JobQueue> owner, JobRef job);
void Distribute(uint64 now);
void Clear();
private:
USE_LOCK;
PriorityQueue<TimerItem> _items;
Atomic<bool> _distributing = false;
};
먼저, JobQueue 와 Job 을 들고 있는 JobData 를 만들어 준다. 순환 참조 문제를 해결하기 위해, JobQueue 는 WeakPtr 로 참조하도록 한다.
TimerItem 은 executeTick 과 JobData 에 대한 생포인터를 갖고 있고, 이 아이템은 JobTimer 클래스에서 PriorityQueue 로 정의된 _item 큐에 삽입될 것이다. 정렬 기준은, executeTick 이 더 작은 녀석부터 Pop 이 될 것이다.
_distributing 은, 현재 일감이 분배된 상태인지를 체크하는데 사용되는 flag 이다.
이제 실제 구현을 보자.
JobTimer.cpp
void JobTimer::Reserve(uint64 tickAfter, weak_ptr<JobQueue> owner, JobRef job)
{
const uint64 executeTick = ::GetTickCount64() + tickAfter;
JobData* jobData = ObjectPool<JobData>::Pop(owner, job);
WRITE_LOCK;
_items.push(TimerItem{ executeTick, jobData });
}
void JobTimer::Distribute(uint64 now)
{
// 한 번에 1 쓰레드만 통과
if (_distributing.exchange(true) == true)
return;
Vector<TimerItem> items;
{
WRITE_LOCK;
while (_items.empty() == false)
{
const TimerItem& timerItem = _items.top();
if (now < timerItem.executeTick)
break;
items.push_back(timerItem);
_items.pop();
}
}
for (TimerItem& item : items)
{
if (JobQueueRef owner = item.jobData->owner.lock())
owner->Push(item.jobData->job);
ObjectPool<JobData>::Push(item.jobData);
}
// 끝났으면 풀어준다
_distributing.store(false);
}
void JobTimer::Clear()
{
WRITE_LOCK;
while (_items.empty() == false)
{
const TimerItem& timerItem = _items.top();
ObjectPool<JobData>::Push(timerItem.jobData);
_items.pop();
}
}
먼저, Reserve 를 보면... 해당 Job 이 실행되기를 원하는 Tick 값과 JobData 를 큐에 넣어줌으로써, Job 이 실행되는 것을 예약하고 있음을 알 수 있다.
실제로 Job 을 배분하는 것은 Distribute 에서 일어나고 있는데... 일단 쓰레드 1개에게 일감이 배분되었으면 바로 return 을 때려 준다.
만약 배분된 상태가 아니라면, _items 큐를 돌면서 예약된 Job 을 보고 Tick 이 실행할 만한 상태라면 꺼내서 Vector 에 옮겨 준다. 참고로, LOCK 을 거는 것을 최소화하기 위해 item 들을 벡터에 복사하고, LOCK 이 없는 상태에서 실행을 해 주는 것이다.
그리고 주석을 보면, '한 번에 1 쓰레드만 통과' 라는 내용이 있는데... 이건 왜 체크해야 하는 것일까?
여러 쓰레드에게 distribute 가 가능하다고 가정해 보자. 사실 LOCK 이 걸리는 부분은 Job 을 꺼내는 부분인데... 아이템을 실제로 Execute 해주기 전에, 약간의 렉이 걸려서 다른 쓰레드가 뒤늦게 꺼낸 Job 이 먼저 '실행' 된다면...?
순차적으로 실행이 되어야 하는 Job 들의 순서가 꼬이는 상황이 발생하게 될 수도 있을 것이다! 😮 물론 이런 상황은 거의 발생하지는 않지만, 미리 방지해 주는 것이 좋으므로, distribute 받는 쓰레드는 한 번에 1개만 가능하도록 만들었다.
이제 JobQueue 는 DoAsync 대신, DoTimer 같은 함수로 JobTimer 를 통해 Job 을 예약만 해주면 될 것이다.
void DoTimer(uint64 tickAfter, CallbackType&& callback)
{
JobRef job = ObjectPool<Job>::MakeShared(std::move(callback));
GJobTimer->Reserve(tickAfter, shared_from_this(), job);
}
template<typename T, typename Ret, typename... Args>
void DoTimer(uint64 tickAfter, Ret(T::* memFunc)(Args...), Args... args)
{
shared_ptr<T> owner = static_pointer_cast<T>(shared_from_this());
JobRef job = ObjectPool<Job>::MakeShared(owner, memFunc, std::forward<Args>(args)...);
GJobTimer->Reserve(tickAfter, shared_from_this(), job);
}
그럼 이제 GameServer 에서는 아래와 같이 Job 을 간단하게 예약해 줄 수 있다! 😉
int main()
{
GRoom->DoTimer(1000, [] { cout << "Hello 1000" << endl; });
GRoom->DoTimer(2000, [] { cout << "Hello 2000" << endl; });
GRoom->DoTimer(3000, [] { cout << "Hello 3000" << endl; });
}
마지막으로, 메모리 Leak 방지를 위해, 클라이언트 연결이 종료되었을 때, GameSession 에서 메모리가 제대로 해제되도록 수정해 주자.
GameSession.h
class GameSession : public PacketSession
{
public:
~GameSession()
{
cout << "~GameSession" << endl;
}
virtual void OnConnected() override;
virtual void OnDisconnected() override;
virtual void OnRecvPacket(BYTE* buffer, int32 len) override;
virtual void OnSend(int32 len) override;
public:
Vector<PlayerRef> _players;
// 아래 추가
PlayerRef _currentPlayer;
weak_ptr<class Room> _room;
};
GameSession.cpp
void GameSession::OnDisconnected()
{
GSessionManager.Remove(static_pointer_cast<GameSession>(shared_from_this()));
// 아래 코드 추가
if (_currentPlayer)
{
if (auto room = _room.lock())
room->DoAsync(&Room::Leave, _currentPlayer);
}
_currentPlayer = nullptr;
_players.clear();
}
이제 연결이 끊겼을 때, 현재 플레이어가 속한 룸에서 플레이어를 없애 줄 수 있게 되었다 😁
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 7-2. DB Bind (0) | 2023.12.21 |
---|---|
[C++ 게임 서버] 7-1. DB Connection (0) | 2023.12.21 |
[C++ 게임 서버] 6-6. JobQueue #5 (0) | 2023.12.20 |
[C++ 게임 서버] 6-5. JobQueue #4 (0) | 2023.12.20 |
[C++ 게임 서버] 6-4. JobQueue #3 (0) | 2023.12.20 |