[Networking] 04 멀티 스레딩

해당 포스팅은 배현직님의 [게임 서버 프로그래밍 교과서] 책의 내용을 바탕으로 작성되었습니다.

1.8 싱글 스레드 게임 서버

대부분의 상용 게임 서버는 CPU가 여러 개의 코어로 구성되어 있다. 따라서 게임 서버를 싱글스레드로 구동하는 경우 비효율적이므로 싱글 스레드 서버를 구동하는 경우 CPU의 개수만큼 프로세스를 띄우는 것이 일반적이다.

싱글스레드로 게임 서버를 만드는 경우, 디스크에서 플레이어 정보를 로딩할 때 발생하는 디바이스 타임을 처리하는 과정에서 큰 시리얼 병목 현상이 발생한다. 이를 해결하고자 비동기 함수나 코루틴 같은 것을 사용하기도 한다. 부득이한 경우가 아니라면 멀티스레드를 이용하는 것이 좋다.


1.9 멀티 스레드 게임 서버

게임에서 멀티스레드 서버를 개발하는 경우는 주로 다음과 같다.

1. 서버 프로세스를 많이 띄우기 곤란할 때, 예를 들어 프로세스당 로딩해야 하는 게임 정보(맵, 데이터 등)의 용량이 매우 클 때.
2. 서버 한 대의 프로세스가 여러 CPU의 연산량을 동원해야 할 만큼 많은 연산을 할 때.
3. 코루틴이나 비동기 함수를 쓸 수 없고 디바이스 타임이 발생할 때.
4. 서버 인스턴스를 서버 기기당 하나만 두어야 할 때.
5. 서로 다른 방이 같은 메모리 공간을 액세스해야 할 때.

보통의 게임 서버에서는 주로 방 단위로 잠금 범위를 설정하는 것이 적정하다.

게임 서버 메인의 데이터 구조는 클래스 1개로 표현할 수 있다. 그리고 각 방도 클래스 1개로 표현 가능하다. 게임 서버 메인 클래스는 방 목록을 가지며, 방 목록에는 각 방이 들어간다.

코드 예시는 아래와 같다.

// 싱글스레드 서버
class Singlethread_Gameserver
{
    class Room
    {
        String m_roomName;
        List<Player> m_players;
        List<Character> m_characters;
    }
    map<PlayerID, shared_ptr<Room>> m_roomList;
    String m_serverName;
}

// 멀티스레드 서버
class Multithread_Gameserver
{
    class Room
    {   // 각 방 안의 데이터들을 보호한다.
        CriticalSection m_critSec;
        String m_roomName;
        List<Player> m_players;
        List<Character> m_characters;
    }
    map<PlayerID, shared_ptr<Room>> m_roomList;
    String m_serverName;
    // 서버 메인과 방 목록을 보호한다.
    // 방 안의 데이터는 메인에서 보호하지 않는다.
    CriticalSection m_critSec;
}

게임 서버 메인은 방 목록이나 방의 공통 데이터를 보호하지만 갖고 있는 방 안의 뮤텍스가 보호하는 데이터까지는 보호하지 않는다. 그리고 플레이어 행동에 대한 처리는 각 방을 잠근 후에 한다.

게임 서버에서 플레이어 A에 대한 처리를 할 때는 다음과 같이 작동한다.

  1. 공통 데이터(방 목록 등)을 잠근다.
  2. 플레이어 A가 들어간 방을 방 목록에서 찾는다.
  3. 공통 데이터를 잠금 해제한다.
  4. 찾은 방을 잠근다.
  5. 플레이어 A의 방 안에서 처리를 한다.
  6. 방을 잠금 해제한다.

이 과정을 간단히 코드로 표현하면 다음과 같다.

GameServer.DoSomething(playerID)
{
    lock(m_critSec);
    room = m_roomList.find(player);
    Unlock(m_critSec);
    lock(room.m_critSec);
    room.DoSomething(playerID);
    Unlock(room.m_critSec);
}

멀티스레드 게임 서버를 만들 때 크게 주의할 점은 시리얼 병목과 교착 상태이다. 특히 파일을 액세스할 때 자주 잠그는 뮤텍스를 잠근 채로 액세스하는 경우 성능 저하가 자주 발생한다.