C#

JobQueue #1

monstro 2025. 1. 22. 16:13
728x90
반응형

Command 패턴을 구현하면서 최대한 C# 문법을 사용하겠습니다.

또한 Command 패턴을 사용하므로 이전처럼 직접적인 Action을 수행하지 않고

최대한 JobQueue에 기록하도록 기존의 코드를 수정할 필요가 있습니다.

 

1) JobQueue

public interface IJobQueue
{
    void Push(Action job);
}

public class JobQueue : IJobQueue
{
    Queue<Action> _jobQueue = new Queue<Action>();
    object _lock = new object();
    bool _flush = false;

    public void Push(Action job)
    {
        bool flush = false;

        // 락을 걸어 비동기적으로 스레드들이 작업을 Queue에 집어넣음
        lock (_lock)
        {
            _jobQueue.Enqueue(job);
            // 이때, _flush가 false라면, true로 바꿔주고 flush를 _flush값으로 초기화
            if (_flush == false)
                flush = _flush = true;
        }

        // flush의 값이 참이라면 Worker Thread가 Queue에 저장된 작업을 수행
        if (flush)
            Flush();
    }

    void Flush()
    {
        while (true)
        {
            Action action = Pop();
            // Action이 null이라면 얼리 리턴
            if (action == null)
                return;

            // Action이 있다면 수행
            action.Invoke();
        }
    }

    Action Pop()
    {
        lock (_lock)
        {
            // Job Queue가 비어있다면 _flush를 false로 바꿔서
            // 바로 다음에 들어오는 작업을 바로 자동으로 처리할 수 있도록 함
            if (_jobQueue.Count == 0)
            {
                _flush = false;
                return null;
            }

            return _jobQueue.Dequeue();
        }
    }
}

 

우선, JobQueue의 역할을 수행할 새로운 클래스를 하나 생성하겠습니다.

JobQueue에서는 해야 할 동작 C#의 동작 단위인 Action으로 기록합니다.

이때 JobQueue에서 _flush가 true라면 워커 스레드가 작업을 자동으로 수행하게 됩니다.

 

JobQueue에서 내부적으로 동작을 수행하는 경우 Lock을 사용하므로

기존의 GameRoom의 로직에서 Lock을 전부 삭제하도록 하겠습니다.

 

2) GameRoom

class GameRoom : IJobQueue
{
    List<ClientSession> _sessions = new List<ClientSession>();
    object _lock = new object();
    JobQueue _jobQueue = new JobQueue();

    public void Push(Action job)
    {
        _jobQueue.Push(job);
    }

    public void Broadcast(ClientSession session, string chat)
    {
        S_Chat packet = new S_Chat();
        packet.playerID = session.clientSessionID;
        packet.chat = $"{chat} I am {packet.playerID}";
        ArraySegment<byte> segment = packet.Write();

       foreach (ClientSession eachSession in _sessions)
            eachSession.Send(segment);
    }

    public void Enter(ClientSession session)
    {
        _sessions.Add(session);
        session.Room = this;
    }

    public void Leave(ClientSession session)
    {
        _sessions.Remove(session);
    }
}

 

GameRoom의 경우, IJobQueue 인터페이스를 상속받아 동작하게 됩니다.

따라서 순수가상함수인 Push를 오버라이드하도록 합니다.

 

3) 서버의 PacketHandler

class PacketHandler
{
    // 인자는 어떤 세션에서 , 어떤 패킷이 조립되었는지를 의미함
    public static void C_ChatHandler(PacketSession session, IPacket packet)
    {
        C_Chat chatPacket = packet as C_Chat;
        ClientSession clientSession = session as ClientSession;

        if (clientSession.Room == null)
            return;

        GameRoom room = clientSession.Room;
        room.Push(
            () => clientSession.Room.Broadcast(clientSession, chatPacket.chat)
        );
    }
}

 

기존의 PacketHandler에서 액션을 수행하는 대신

액션을 수행하는 람다를 선언하여 해당 람다 함수를 GameRoom의 JobQueue에 기록(=저장)합니다.

이때 GameRoom이 null로 설정되는 경우발생하는 에러를 방지하고자

GameRoom을 인스턴스화하여 지역변수로서 사용하겠습니다.

 

4) ClientSession

class ClientSession : PacketSession
{
    // Session ID
    public int clientSessionID { get; set; }
    // 현재 어떤 방에 있는지
    public GameRoom Room { get; set; }

    public override void OnConnected(EndPoint endPoint)
    {
        System.Console.WriteLine($"OnConnected : {endPoint}");

        Server.Room.Push(() => Server.Room.Enter(this));
    }

    public override void OnRecvPacket(ArraySegment<byte> buffer)
    {
        PacketManager.Instance.OnRecvPacket(this, buffer);
    }

    public override void OnDisconnected(EndPoint endPoint)
    {
        SessionManager.Instance.Remove(this);

        if (Room != null)
        {
            GameRoom room = Room;
            room.Push(() => room.Leave(this));
            Room = null;
        }

        System.Console.WriteLine($"OnDisconnected : {endPoint}");
    }

    public override void OnSend(int numOfBytes)
    {
        System.Console.WriteLine($"Transferred bytes : {numOfBytes}");
    }
}

 

ClientSession에서도 OnDisconnected에서 

GameRoom을 바로 Leave하는 액션을 수행하지 않고 대신 해당 작업을 람다함수로 만들고

이를 JobQueue에 저장하겠습니다.

마찬가지의 이유로 GameRoom이 null이 되는 경우를 방지하고자

GameRoom을 지역변수로 캐싱하여 사용하겠습니다.

 

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

 

GameRoom이 null이 되는 경우도 잘 방지하였기에 

연결 도중에 ClientSession이 끊어지더라도 문제없이 OnDisconnected됩니다.

 

최종적으로 이전 사례와 다르게 스레드들이 작업을 받고 수행하는 동작없이 

기록만 하므로 스레드들이 무수히 대기하는 일이 발생하지 않게 됩니다.

728x90
반응형

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

패킷을 모아서 전송하기  (0) 2025.01.27
JobQueue #2  (0) 2025.01.22
Command 패턴  (0) 2025.01.22
채팅 테스트 #2  (0) 2025.01.21
채팅 테스트 #1  (0) 2025.01.21