Job Queue
지난 시간에는 GameRoom 스크립트에서 BroadCast 메서드가 foreach문을 통해 Send를 계속했을 때 나타나는 문제점을패킷 모아보내기라는 기법을 활용하여 처리하는 시간을 가졌다. 그렇지만 GameRoom과 같이 무슨무슨 룸들이 점차 많아지고, 각각의 룸들이 서로 다른 대기시간을 가지며 실행이 된다고 하면 지금과 같은 방법으로는 처리할 수 없기 때문에 일감 관리를 도와주는 Timer를 통해 이를 해결해보는 시간을 가진다.
🏃 틱을 사용한 처리
현재 시간과 roomTick이라는 변수를 통해 두 변수의 시간을 비교하며 실행할지, 말지 결정하는 방법이다.
위와 같은 방법으로 코드를 구성한다면 각 룸에 따라 메인 루프에서 틱에 맞춰서 동작하도록 구성할 수 있다. 다만 메인 메서드의 while 문에서 모든 연산을 처리하는 방법은 불필요한 연산작업을 계속 해주어야 하기 때문에 효율적이지 못하다.
🤯 JobTimer
따라서 예약시스템인 JobTimer를 만들어 일정 시간 뒤에 실행하는 방법을 사용하고자 한다. 이를 위해서는 우선 순위 큐를 사용하는데, 이는 2강 자료구조 시간에 만들었던 우선 순위큐를 그대로 가져와 사용한다.
📜 PriorityQueue.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
public class PriorityQueue<T> where T : IComparable<T>
{
List<T> _heap = new List<T>();
public int Count { get { return _heap.Count; } }
// O(logN)
public void Push(T data)
{
// 힙의 맨 끝에 새로운 데이터를 삽입한다
_heap.Add(data);
int now = _heap.Count - 1;
// 도장깨기를 시작
while (now > 0)
{
// 도장깨기를 시도
int next = (now - 1) / 2;
if (_heap[now].CompareTo(_heap[next]) < 0)
break; // 실패
// 두 값을 교체한다
T temp = _heap[now];
_heap[now] = _heap[next];
_heap[next] = temp;
// 검사 위치를 이동한다
now = next;
}
}
// O(logN)
public T Pop()
{
// 반환할 데이터를 따로 저장
T ret = _heap[0];
// 마지막 데이터를 루트로 이동한다
int lastIndex = _heap.Count - 1;
_heap[0] = _heap[lastIndex];
_heap.RemoveAt(lastIndex);
lastIndex--;
// 역으로 내려가는 도장깨기 시작
int now = 0;
while (true)
{
int left = 2 * now + 1;
int right = 2 * now + 2;
int next = now;
// 왼쪽값이 현재값보다 크면, 왼쪽으로 이동
if (left <= lastIndex && _heap[next].CompareTo(_heap[left]) < 0)
next = left;
// 오른값이 현재값(왼쪽 이동 포함)보다 크면, 오른쪽으로 이동
if (right <= lastIndex && _heap[next].CompareTo(_heap[right]) < 0)
next = right;
// 왼쪽/오른쪽 모두 현재값보다 작으면 종료
if (next == now)
break;
// 두 값을 교체한다
T temp = _heap[now];
_heap[now] = _heap[next];
_heap[next] = temp;
// 검사 위치를 이동한다
now = next;
}
return ret;
}
public T Peek()
{
if (_heap.Count == 0)
{
return default(T);
}
return _heap[0];
}
}
}
⌛ JobTimerElem
JobTimerElem을 구조체(struct) 형식으로 선언하고, IComparable 인터페이스를 상속 받아준다. 이 때 두 개의 변수를 만들어준다. 실행 시간을 계산할 execTick, 업무 그 자체를 가져올 action 이다.
IComparable 인터페이스의 필수 구성 요소인 CompareTo 메서드를 통해 받아온 시간 - 실행시간을 계산하여 우선순위가 더 빠른 항목을 체크하여 준다.
⌛ JobTimer Class
해당 클래스를 구성하기 위해 몇가지 요소들을 선언해준다.
1. 우선순위 큐를 JobTimerElem 타입으로 선언
2. 멀티쓰레드 환경에 대비한 lock 오브젝트 선언
3. 보다 편리한 사용을 위한 JobTimer를 static 선언
❇️ Push()
Push 메서드를 만들어 일감을 받기 위한 action, 추가 시간을 위한 int tickAfter를 매개 변수로 받아준다. 이 때 tickAfter = 0의 값을 가지도록 하는데, 해당 값을 입력하지 않으면 바로 실행시키도록 한다.
이 후, JobTimerElem 타입의 job을 선언해주고, 받아온 일감과 시간을 설정하여 lock을 이용해 Push를 진행해준다.
❇️ Flush()
일감을 꺼내올 일은 잘 없을 수 있기 때문에 Pop은 따로 구현하지 않고, JobTimer 안에서 일감을 직접 처리하도록 Flush()를 구현하여 준다. while 문을 통해 루프를 돌며 현재 시간을 받아오고, JobTimerElem에서 일감을 가지고와 lock 안에서 실행시키도록 한다.
단, 무작정 Pop을 할 수 없으니 조건을 걸어준다.
1. 데이터가 없고, 비어있는 상태 (=일감이 없는 상태)
if (_pq.Count == 0)
break;
2. 현재 시간보다 실행 시간이 많이 남았을 때
if (job.execTick > now)
break;
Peek는 다음 실행해야 할 일감의 값을 가져오지는 않고, 어떠한 일감인지를 확인하는 용도이다. 따라서 위 조건들을 모두 통과했다면 _pq.Pop()을 통해 값을 가져오고 job.action.Invoke를 통해 일감을 실행시켜 준다.
⌛ 적용
Server 프로젝트의 Program 스크립트에 FlushRoom 메서드를 새롭게 static으로 만들어주고, 해당 메서드 내부에서 일감을 넣고 동작을 예약시켜준다.
그 뒤에 메인 루프가 실행되기 전에, FlushRoom을 실행시켜 일감을 넣어주고, 예약해준 뒤에 이 후 루프를 돌며 일감 처리를 진행하도록 한다.
이렇게 해주면 JobTimer를 통한 일감 처리 진행이 모두 마무리 된다. Log n의 시간 복잡도를 가진 우선순위 큐 덕분에 빠른 탐색도 가능하고, 현재 진행 가능한 일감만 체크하면서 진행하기 때문에 보다 효율적으로 동작할 수 있다.
마치며 😎
이렇게해서 게임 서버 개론, 멀트쓰레드 프로그래밍, 네트워크 프로그래밍, 패킷 직렬화, Job Queue 까지 모두 학습해보았다. 다음 시간에서부터는 이 모든것들을 유니티에 직접 연동하여 동작하는 시간을 가져보도록 한다.
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 4-6-2. 유니티 연동 : 유니티 연동 #2 (1) | 2024.01.03 |
---|---|
Part 4-6-1. 유니티 연동 : 유니티 연동 #1 (1) | 2024.01.02 |
Part 4-5-6. Job Queue : 패킷 모아보내기 (0) | 2023.12.28 |
Part 4-5-5. Job Queue : Job Queue #2 (0) | 2023.12.28 |
Part 4-5-4. Job Queue : Job Queue #1 (0) | 2023.12.27 |