C# - 게임 서버에서 프레임 단위로 클라이언트 요청을 처리한다. async-await 사용

출처

/*
var thread = GameLoopThreadPool.GetThread();
while (true)
{
    // frameAction 안에서 AI의 행동 선택 처리나 쌓인 커맨드를 기초로 데미지를 주거나 회복 시키는 코드가 동작
    var shouldContinue = frameAction(this);
    if (!shouldContinue) break;
    await thread.NextFrame();
}
*/

public class GameLoopThread
{
    const int InitialSize = 16;

    Thread thread; // Thread를 사용한다.
    int threadNumber = 0;

    readonly object gate = new object();
    int frameMilliseconds;

    // 2개의 배열(축소하지 않는)을 큐로 사용하여 lock을 최소한으로 하고 배열 확장/축소 비용, 그리고 루프 속도를 번다
    bool isQueueA = true;
    int queueACount = 0;
    Action[] queueA = new Action[InitialSize];
    int queueBCount = 0;
    Action[] queueB = new Action[InitialSize];

    public GameLoopThread(int frameMilliseconds, int threadNumber)
    {
        this.frameMilliseconds = frameMilliseconds;
        this.thread = new Thread(new ParameterizedThreadStart(RunInThread), 32 * 1024) // maxStackSize은 작아도 상관 없다
        {
            Name = "GameLoopThread" + threadNumber,
            Priority = ThreadPriority.Normal,
            IsBackground = true
        };
        this.thread.Start(this);
    }

    static void EnsureCapacity(ref Action[] array, int count)
    {
        if (count == array.Length)
        {
            var newLength = count * 2;
            var newArray = new Action[newLength];
            Array.Copy(array, newArray, array.Length);
            array = newArray;
        }
    }

    static void RunInThread(object objectSelf)
    {
        var self = (GameLoopThread)objectSelf;
        self.Run();
    }

    void Run()
    {
        var sw = Stopwatch.StartNew();
        while (true) // 실제 코드에서는 밖에서 종료 할 있도록 문을 준비하고 있다
        {
            sw.Restart();
            Action[] useQueue;
            int useQueueCount;

            lock (gate)
            {
                if (isQueueA)
                {
                    useQueue = queueA;
                    useQueueCount = queueACount;
                    queueACount = 0;
                    isQueueA = false;
                }
                else
                {
                    useQueue = queueB;
                    useQueueCount = queueBCount;
                    queueBCount = 0;
                    isQueueA = true;
                }
            }

            for (int i = 0; i < useQueueCount; i++)
            {
                useQueue[i].Invoke(); // 이게 예외를 내는 것은 허락하지 않는다. 라는 것을 호출 측에서 보증해 준다
                useQueue[i] = null;
            }
            useQueue = null;

            sw.Stop();

            if (useQueueCount != 0)
            {
                // Datadog 에 처리 시간을 모니터링을 넣어 둔다
                var tags = new[] { "gameloopthreadnumber:no-" + threadNumber };
                DatadogStats.Default.Gauge("GameLoopThreadRun.avg", sw.Elapsed.TotalMilliseconds, sampleRate: 0.1, tags: tags);
                DatadogStats.Default.Increment("GameLoopThreadRun.count", sampleRate: 0.1, tags: tags);
                DatadogStats.Default.Gauge("GameLoopThreadRun.ProcessCount", useQueueCount, sampleRate: 0.1, tags: tags);
            }

            // 정확한 게임 루프를 따르는 경우는  시간의 차리를 취하면서 Sleep(1) 으로 처리 회전 여부를 넣을지 어떨지 체크를 넣는다
            // 루프를 돌리는 간격의 애매함을 허럭하는 경우는 이대로도 문제 없다
            Thread.Sleep(frameMilliseconds);
        }
    }

    void RegisterCompletion(Action continuation)
    {
        lock (gate)
        {
            if (isQueueA)
            {
                EnsureCapacity(ref queueA, queueACount);
                queueA[queueACount++] = continuation;
            }
            else
            {
                EnsureCapacity(ref queueB, queueBCount);
                queueB[queueBCount++] = continuation;
            }
        }
    }

    public GameLoopAwaiter NextFrame()
    {
        return new GameLoopAwaiter(this);
    }

    public struct GameLoopAwaiter : ICriticalNotifyCompletion, INotifyCompletion
    {
        GameLoopThread thread;

        public GameLoopAwaiter(GameLoopThread thread)
        {
            this.thread = thread;
        }

        public bool IsCompleted => false;

        public void GetResult()
        {
        }

        public GameLoopAwaiter GetAwaiter()
        {
            return this;
        }
		
        // await를 호출하면 OnCompleted 시리즈가 호출되어 GameLoopThread 에 Action을 읽는다
        public void OnCompleted(Action continuation)
        {
            thread.RegisterCompletion(continuation);
        }

        public void UnsafeOnCompleted(Action continuation)
        {
            thread.RegisterCompletion(continuation);
        }
    }
}

이 글은 2019-06-02에 작성되었습니다.