Server programming

04_07_Job Queue_JobTimer

devRiripong 2023. 5. 17.
반응형

개요

이 문서는 서버 프로그래밍에서 시간 관리를 효율적으로 할 수 있는 방법에 대해 설명하고 있습니다. 주로 게임 서버에서 여러 객체가 동작해야 하는 경우를 대상으로 이야기하고 있으며, 이를 위한 두 가지 접근 방식을 제시하고 있습니다.


1. Tick을 이용하는 방법: 가장 간단한 방법으로, 주기적인 시간 간격으로 객체의 동작을 갱신하는 방식입니다. 이 방식의 장점은 간단하고, 멀티스레드에 대한 복잡한 이해 없이도 쉽게 사용할 수 있다는 것입니다. 그러나, 서버에 여러 객체가 존재하고 각 객체마다 갱신 타이밍이 다른 경우 while 문 안에 모든 로직을 넣어야 하므로 복잡해질 수 있습니다.
2. 예약 시스템을 이용하는 방법: 더욱 세련된 방법으로, PriorityQueue를 이용한 예약 시스템을 제안합니다. 이 방식은 특정 동작을 나중에 실행하도록 예약하는 방식이며, 우선순위 큐를 이용하여 효율적으로 관리됩니다. 이 시스템은 Unity의 Coroutine이나 JavaScript의 setTimeout 같은 기능과 유사합니다.


서버측에서 이런 시스템을 구현하는 이유는, 서버는 많은 수의 클라이언트를 동시에 처리해야 하므로, 시간 관리가 중요하기 때문입니다. 특히, 게임 서버에서는 많은 수의 게임 객체가 동시에 동작해야하므로, 이런 시스템이 효과적일 수 있습니다.


이 문서에서는 이 예약 시스템을 구현하는 방법을 C# 코드로 구체적으로 설명하고 있습니다. 구현된 예약 시스템은 싱글톤 패턴으로 구현되어 있어, 서버 전체에서 하나의 인스턴스만 존재하게 하여 모든 객체가 이를 공유할 수 있습니다.


이렇게 시간 관리를 중앙에서 할 경우, 각 객체가 개별적으로 시간을 체크하는 것보다 훨씬 효율적일 수 있습니다. 추가적으로, 시간이 많이 남은 작업들과 시간이 적게 남은 작업들을 분리하여 관리하면 더욱 최적화할 수 있습니다.

 

 

 

 

 

 

기존Server의 Main

while (true)
{
    Room.Push(() => Room.Flush());
    Thread.Sleep(250); 
}

지금은 Room하나지만

객체가 여러개가 되어서 Flush 하는 타이밍이 다 다르다면 어떻게 시간 관리를 해야 할까?

1.Tick을 이용: 가장 쉬움.

int roomTick = 0; 
while (true)
{
    int now = System.Environment.TickCount; 
    if (roomTick < now)
    {
        Room.Push(() => Room.Flush());
        roomTick = now + 250; 
    }    
}

하지만 만약 Room뿐만 아니라 다른 객체들이 늘어난다면?

roomTick과 같은 tick 인자를 늘리고, while 안에 내용을 늘리는 방법을 사용한다.

while문 안에서 모든 게임 컨텐츠 로직을 넣어서 하는 방법이 있다. 멀티 스레드에 대한 개념이 없어도 편리하게 관리 할 수 있다는 게 장점이다.

하지만 조금 더 세련된 방법은 없을까?

2.예약 시스템: 좀 더 세련된 방법

위와 같이 Tick을 이용해 매 프레임마다 불필요한 if 문 비교를 하는 건 비효율적이다.

코루틴 같이 wait for second로 시간 지나면 호출 되게 우선순위 큐를 이용해 만들어 보자.

2_1. 예약 시스템 구현

2_1_1. ServerCore에 PriorityQueue 클래스 파일을 생성한다.(컨텐츠 단에도 해도 됨)

2_1_1_1. PriorityQueue 알고리즘을 복붙 한다.

2_1_1_2. Count로 _heap.Count를 이용할 수 있게 get 인터페이스 이용 할 수 있게 수정한다.

2_1_1_3. Peek 인터페이스를 구현한다.

2_1_2. Server에 JobTimer파일을 생성한다.

2_1_2_1. JobTimer를 구현한다.

using System;
using System.Collections.Generic;
using System.Text;
using ServerCore;

namespace Server
{
    struct JobTimerElem : IComparable<JobTimerElem>
    {
        public int execTick; // 실행 시간
        public Action action; 

        public int CompareTo(JobTimerElem other)
        {
            return other.execTick - execTick; 
        }
    }

    class JobTimer
    {
        PriorityQueue<JobTimerElem> _pq = 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)
            {
                _pq.Push(job); 
            }
        }

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

                JobTimerElem job; 

                lock(_lock)
                {
                    if (_pq.Count == 0)
                        break;

                    job = _pq.Peek();
                    if (job.execTick > now)
                        break;

                    _pq.Pop(); 
                }

                job.action.Invoke(); 
            }
        }
    }
}

2_1_3. Server의 Program에서 while문 안을 JobTimer를 사용하게 수정

2_1_3_1. 기존 작업 삭제

int roomTick = 0;
while (true)
{
    int now = System.Environment.TickCount; 
    if (roomTick < now)
    {
        Room.Push(() => Room.Flush());
        roomTick = now + 250; 
    }                
}

에서

while (true)
{
        Room.Push(() => Room.Flush());  
}

2_1_3_2. FlushRoom 함수 구현

static void FlushRoom()
{
    Room.Push(() => Room.Flush());
    JobTimer.Instance.Push(FlushRoom, 250); 
}

2_1_3_3. Main에서 FlushRoom 호출

static void Main(string[] args)
{
    // DNS ( Domain Name System )
    string host = Dns.GetHostName();
    IPHostEntry ipHost = Dns.GetHostEntry(host);
    IPAddress ipAddr = ipHost.AddressList[0];
    IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
    // ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호

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

    FlushRoom(); 

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

or

    //FlushRoom(); 
    JobTimer.Instance.Push(FlushRoom);

2_2. 장점

관리하는 객체가 3000개면 3000번 tickCount를 체크해야 하는데 중앙에서 관리하니 Log N 복잡도로 가능

2_3. 더 최적화 한다면

예를 들어 5분 이상 남은 여유롭게 시간이 남은 그런 잡들은 이전과 마찬가지로 이런 우선순위 큐 같은 데서 관리를 하고 그게 아니라, 조금 시간이 임박한 애들은 이런 식으로 리스트로 관리

3.결론

클라이언트는 유니티의 Coroutine의 wait for seconds 같이 중앙에서 관리 하는 기능이 있으니 또 만들 필요 없지만, 서버 쪽에서는 이렇게 중앙 관리 시스템이 있으면 굉장히 좋다

 

출처: https://inf.run/HRQ2

반응형

댓글