C#

JobTimer

monstro 2025. 1. 27. 17:37
728x90
반응형

현재 상태의 코드에서는 하나의 GameRoom 만이 존재하여 문제없이 동작하지만,

만약 현재 GameRoom 외에다른 용도의 Room들이 추가되면 

동시에 개별 Room들의 작업을 처리해야 하므로 부하가 발생하게 됩니다.

 

따라서 이번 포스트에서는 해당 문제를 해결하기 위한 작업을 수행해보겠습니다.

 

1) Server - 메인 스레드에서 Tick을 통한 처리

class Server
{
    
	.
	.
	.

    static void Main(string[] args)
    {
        
		.
		.
		.
        
        int roomTick = 0;
        while (true)
        {
            int now = System.Environment.TickCount;
            if (roomTick < now)
            {
                Room.Push(() => Room.Flush());
                roomTick = now + 250;
            }
        }
    }
}

 

첫 번째 방법은 Server의 메인 스레드에서 roomTick이라는 지역 변수를 통해 
현재 시간과 roomTick간의 시간 차이를 비교하여 실행하는 방식입니다.

이 방법을 통해 코드를 구성하는 경우,
개별 Room 별로 매 프레임에 맞춰서 동작하도록 구성할 수 있습니다.
다만 무한히 반복되는 while 문에서 모든 연산을 처리하게 되어

불필요한 연산 작업을 계속 수행하므로 비효율적입니다.

 

따라서 이런 Room들간의 작업을 조정해주기 위한 JobTimer를 사용하겠습니다.
JobTimer를 구현하는 방법에 있어서는 우선순위 큐를 사용하여 구현하도록 하겠습니다.

 

2) 두번째 방법 - JobTimer

2 - 1) PriorityQueue

public class PriorityQueue<T> where T : IComparable<T>
{
    List<T> _heap = new List<T>();

    public int Count { get { return _heap.Count; } }

    // 새로운 Job을 추가하는 함수
    public void Push(T data)
    {
        _heap.Add(data);

        int now = _heap.Count - 1;

        // 우선순위 비교
        while (now > 0)
        {
            int next = (now - 1) / 2;
            
            // 새로운 Job의 우선순위가 낮은 경우에 break
            if (_heap[now].CompareTo(_heap[next]) < 0)
                break; 

            // 새로운 Job의 우선순위가 높은 경우에 교체
            T temp = _heap[now];
            _heap[now] = _heap[next];
            _heap[next] = temp;

            // 검사 위치를 이동한다
            now = next;
        }
    }

    // 보관된 Job을 가져오는 함수
    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;
            // 왼쪽 Job의 우선순위가 현재 Job의 우선순위보다 크면, 왼쪽으로 이동
            if (left <= lastIndex && _heap[next].CompareTo(_heap[left]) < 0)
                next = left;
            // 오른쪽 Job의 우선순위가 현재 Job의 우선순위보다 크면, 오른쪽으로 이동
            if (right <= lastIndex && _heap[next].CompareTo(_heap[right]) < 0)
                next = right;

            // 왼쪽/오른쪽 Job 모두 우선순위가 현재 Job의 우선순위보다 작으면, break
            if (next == now)
                break;

            // 두 값을 교체한다
            T temp = _heap[now];
            _heap[now] = _heap[next];
            _heap[next] = temp;

            // 검사 위치를 이동한다
            now = next;
        }

        return ret;
    }

    // head에 있는 Job을 확인하여 가져오는 함수
    public T Peek()
    {
        if (_heap.Count == 0)
            return default(T);

        return _heap[0];
    }
}

 

2 - 2) Server - JobTimer

struct JobTimerElem : IComparable<JobTimerElem>
{
    public int execTick;    // 실행 시간
    public Action action;   // 수행할 동작(=Job)

    // 다음의 비교 연산을 통해 우선순위가 더 높은 Job을 체크
    public int CompareTo(JobTimerElem other)
    {
        return other.execTick - execTick;
    }
}

class JobTimer
{
    PriorityQueue<JobTimerElem> _priorityQueue = new PriorityQueue<JobTimerElem>();
    object _lock = new object();

    public static JobTimer Instance { get; } = new JobTimer();

    public void Push(Action action, int tickAfter = 0)
    {
        JobTimerElem job;
        job.execTick = System.Environment.TickCount + tickAfter;
        job.action = action;

        lock (_lock)
        {
            _priorityQueue.Push(job);
        }
    }

    public void Flush()
    {
        while (true)
        {
            int now = System.Environment.TickCount;

            JobTimerElem job;

            lock (_lock)
            {
                // JobQueue에 저장된 Job이 없는 상태 == 수행할 Job이 없으므로 Flush 메소드 수행 중지
                if (_priorityQueue.Count == 0)
                    break;

                // Job의 실행시간이 현재 시간보다 큰 경우 == Job이 수행중이므로 Flush 메소드 수행 중지
                job = _priorityQueue.Peek();
                if (job.execTick > now)
                    break;

                _priorityQueue.Pop();
            }

            // 조건문에 결리지 않았다면 Pop된 Job에 연결된 콜백함수 수행
            job.action.Invoke();
        }
    }
}

 

2 - 3) Server

class Server
{
    static Listener serverListener = new Listener();
    // 새로운 채팅방을 위한 GameRoom 생성
    public static GameRoom Room = new GameRoom();

    // 메서드 내부에서 Job을 넣고 콜백함수를 걸어줌
    static void FlushRoom()
    {
        Room.Push(() => Room.Flush());
        JobTimer.Instance.Push(FlushRoom, 250);
    }

    static void Main(string[] args)
    {
        // DNS를 사용
        string host = Dns.GetHostName();
        IPHostEntry ipHost = Dns.GetHostEntry(host);
        IPAddress ipAddr = ipHost.AddressList[0];
        IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

        serverListener.Init(endPoint, () => { return SessionManager.Instance.Generate(); }, 500, 500);
        Console.WriteLine("Listening...");

        // 메인 루프가 실행되기 전, FlushRoom을 실행시켜 Job을 넣어주고, 예약함 
        // 무한 루프를 돌며 넣어준 Job 처리를 진행
        // 다음의 2가지 방법으로 수행가능
        // FlushRoom();
        JobTimer.Instance.Push(FlushRoom);

        while (true)
        {
            JobTimer.Instance.Flush();
        }
    }
}

 

최종 실행 결과는 다음과 같습니다.

728x90
반응형

'C#' 카테고리의 다른 글

유니티 연동 #2  (0) 2025.01.28
유니티 연동 #1  (0) 2025.01.28
패킷을 모아서 전송하기  (0) 2025.01.27
JobQueue #2  (0) 2025.01.22
JobQueue #1  (0) 2025.01.22