Home 게임 서버 프로그래밍 교과서 1장
Post
Cancel

게임 서버 프로그래밍 교과서 1장

멀티스레딩

프로그램, 프로세스, 스레드

  • 프로그램(디스크 등의 저장소)
    • 코드 + 데이터
  • 프로세스(RAM)
    • 프로그램이 실행되어 활동하는 상태
    • 코드 + 데이터 : 프로세스 메모리에 로딩
      • 스택: 현재 실행 중인 함수들의 호출 기록과 사용 중인 로컬 변수들
      • 힙: 동적 할당
  • 멀티프로세싱
    • 프로세스가 여러개 실행
    • 각 프로세스는 독립된 메모리 공간이 있음
  • 스레드
    • 프로세스처럼 명령어를 한 줄씩 실행하는 기본 단위
    • 프로세스와의 차이점은 아래와 같음.
      • 스레드는 한 프로세스 안에 여러 개가 있음.
      • 한 프로세스 안에 있는 스레드는 프로세스 안에 있는 메모리 공간(힙)을 같이 사용할 수 있음
      • 스레드마다 스택을 가짐. (각 스레드에서 실행되는 함수의 로컬 변수들이 스레드마다 있음)
    • 프로그램이 실행되는 기본 단위를 하나의 스레드라고 말할 수 있음.
      • 유일한 스레드를 메인 스레드라고 함
  • 호출 스택
    • 함수 실행이 끝나면 자신을 호출한 지점으로 되돌아가야함. 이 정보가 저장되는 곳이 호출 스택
    • 함수 안에 선언된 지역 변수 또한 스택에 있음.
    • 각 스레드는 실행 지점이 서로 다름.
      • 따라서 각 스레드는 각자 호출 스택을 가짐.
  • 스레드 일생

  • 메인 스레드가 먼저 종료되면, 다른 스레드가 아직 실행중이므로 프로세스는 종료되지 않는다. (좀비 프로세스)

멀티스레드 프로그래밍은 언제?

  1. 오래 걸리는 일 하나와 빨리 끝나는 일을 같이 수행해야 할 때
    • 로딩
      • 그래픽 리소스 로딩 등..
      • 로딩 진행상황 그래프 or 애니메이션 or 미니게임
  2. 어떤 긴 처리를 진행하는 동안 다른 짧은 일을 처리해야 할 때
    • 디스크 액세스하는 스레드는 디스크의 처리 결과가 끝날 때까지 기다려야하는데, 이 시간 동안 CPU는 놀고 있음
      • 이 노는 시간을 분배하여 실행 성능을 개선
  3. CPU를 모두 활용해야 할 때
    • 늘어나는 CPU 코어 개수

스레드 정체

  • 컨텍스트 스위치: 각 스레드를 실행하다 말고 다른 스레드를 마저 실행하는 과정
    • 비용이 듦: 스레드의 상태(스택 등)을 저장, 다른 스레드 선택, 스레드 복원, 실행하던 지점으로 이동
    • 빈도를 줄여도 문제: 끊김 현상
    • 기본적으로 컨텍스트 스위치 실행은 사람이 쾌적하게 느끼는 시간 단위(타임 슬라이스)로 이루어짐
    • os, cpu 등 환경마다 다르지만, 대략 5밀리초
  • CPU 개수와 스레드 개수(비둘기 집의 원리: 비둘기 집 = cpu, 비둘기 = 스레드)
    • 같으면 컨텍스트 스위치가 발생할 이유가 없음
    • 스레드가 더 많으면 발생함
      • 실행 중 상태인 스레드 > CPU: 성능 문제
  • 주의점
    • 프로그램 어느 지점에서 컨텍스트 스위치가 일어나는지
      • 기계어 명령어를 다 실행하고 난 후, 컨텍스트 스위치가 일어남 (데이터 레이스 문제 발생 가능성)
      • 즉, 기계어 명령 단위로 이루어짐

스레드 주의사항

  • 데이터 레이스(경쟁 상태)
    • 여러 스레드가 데이터에 접근하여 그 상태를 예측할 수 없게 하는 것
    • 해결방법: 원자성(atomicity)을 만족하는 코드 작성
      • 항상 변수는 일관성 있는 상태를 유지해야함 (consistency)
      • ‘동기화’라고 부르며, 임계영역과 뮤텍스(상호배제), lock 기법 을 사용

임계 영역과 뮤텍스

  • 다른 스레드가 건드리지 못하게 하는 뮤텍스(상호 배제: mutual exclusion)
    1. X, Y를 보호하는 뮤텍스 MX를 생성
    2. 스레드는 X, Y를 건드리기 전에 MX에 사용권을 요청해야함
    3. 사용권을 얻고 스레드가 X, Y에 접근
    4. 접근이 끝나면 MX에 사용권 해제 요청
  • 뮤텍스 lock 과 unlock 구간을 지정함으로 써 수행됨
  • 이러한 뮤텍스를 임계 영역(critical section)이라고 부름.
    • 윈도: 뮤텍스 와 임계영역… 임계 영역이 처리 속도 빠름
      • 임계영역: 리눅스에서 unnamed 뮤텍스에 해당
    • 리눅스: 뮤텍스… 뮤텍스는 임계 영역과 같은 방식으로 작동할 수 있도록 최적화됨
  • 이러한 lock 과 unlock은 예외처리가 힘듦
    • 구간 중간에 예외가 발생해도 unlock()을 실행할 수 있도록 해야함
    • 해결방법: lock_guard
      • 소멸시 자동으로 잠금해제
      • C#에서는 lock 구문 블록을 이용하면 자동으로 잠금해제됨
  • recursive_mutex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int num = 1;
//...
ThreadProc() {
    {
        lock_guard<recursive_mutex> num_lock(num_mutex);
        n = num;
        num++;
    }
    //...
    if(IsPrimeNumber(n))
    {
        lock_guard<recursive_mutex> primes_lock(primes_mutex);
        primes.push_back(n);
    }
}
  • CPU 개수가 많더라도 100%성능이 아닌 이유
    • 락: 대기 상태로 전환
    • CPU가 메모리 접근(메모리 바운드 시간)
      • 여러 CPU가 접근할 경우, CPU안에서 블로킹이 발생
    • 메모리 접근의 양을 최대한 줄이는 것이 좋음
  • 뮤텍스 구간을 최대한 잘게 나누는 경우
    • 뮤텍스 접근 비용이 크기 때문에 성능 저하 가능성
    • 프로그램이 복잡해질 가능성 (교착상태: deadlock)
  • 구간을 넓게 잡은 경우
    • 싱글 스레드와 마찬가지
  • 적당히 넓게 나누는 것이 좋음
    • 동시에 연산하면 유리한 부분: 잠금 단위
    • 병렬 하지 않아도 성능 영향 주지 않는 부분: 잠금 단위 x

컨텐션(contention)

두 스레드가 동시에 한 데이터를 접근하려고 하는 상황을 의미함.

교착 상태

  • 두 스레드가 서로를 기다리는 상황
    • CPU 사용량이 낮거나 0% (동시접속자 수와 무관)
    • 클라가 서버 이용 못함 (로그인 응답 등)
  • 잠금 순서의 규칙으로… 해결

잠금 순서의 규칙

  • 그래프를 그리고 확인 (잠금 순서 그래프)
    • 거꾸로 잠금한 것이 없는지 확인
  • 재귀 뮤텍스(recursive mutex)는 한 스레드가 뮤텍스를 여러 번 락 할 수 있음
    • 이를 사용하면 잠금 순서 규칙을 지키기 편함
    • 문제: 첫 잠금 순서는 중요함
  • 암달의 법칙
    • 시리얼 병목이 있을 때 CPU 개수가 많을수록 총 처리 효율성이 떨어지는 현상
      • 시리얼 병목이 발생하는 구간을 최소로 줄여야함 (vs의 concurrency visualizer)
  • CUP 타임과 디바이스 타임
    • 디바이스: 네트워크 인터페이스, 디스크 등 뭔가 요청해서 결과를 기다리는 시간을 의미
      • 스레드는 잠자는 상태 (sleep)

싱글스레드 게임 서버

  • CPU 개수만큼 프로세스를 띄움(싱글 스레드 멀티 프로세스)

  • 디스크에서 플레이어 정보를 로딩할 때 발생하는 디바이스 타임을 처리하는 과정에서 큰 시리얼 병목이 일어남
    • 코루틴이나 비동기함수를 사용
  • 방 개수만큼 스레드나 프로세스가 있으면 스레드나 프로세스 간 컨텍스트 스위치 횟수가 증가
    • 동시접속자 수 크게 떨어짐

멀티스레드 게임 서버

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

클래스 표현

  • 게임 서버 메인의 데이터 구조는 클래스 1개로 표현 가능
    • 방 클래스 또한 가능
    • 게임 서버 메인: 방목록
    • 방목록: 각 방
  • 멀티스레드 서버는 아래와 같이 코드를 작성할 수 있음
    • 각 방은 뮤텍스를 가짐: 방 안의 데이터 보호
    • 서버 메인도 뮤틱스를 가짐: 방목록, 공통데이터 보호
1
2
3
4
5
6
7
8
9
10
11
class MyGameServer {
    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_cretSec; // 서버 메인과 방목록을 위한
};
  • 플레이어 행동 처리는 각 방을 잠근 후에 함
    1. 공통 데이터(방목록 등)을 잠금
    2. 플레이어 A가 들어 있는 방을 방 목록에서 찾음
    3. 공통 데이터를 잠금 해제함.
    4. 찾은 방을 잠금
    5. 플레이어 A의 방 안에서 처리를 함
    6. 방을 잠금 해제함
  • 가장 큰 주의할 점은 시리얼 병목과 교착 상태
    • 파일 접근은 특히 주의(뮤텍스 잠근 채로 접근하는 경우 성능 저하 발생)

스레드 풀링

이벤트

세마포어

원자조작

멀티스레드 프로그래밍의 흔한 실수들

출처

게임 서버 프로그래밍 교과서

This post is licensed under CC BY 4.0 by the author.

[C++] Effective C++ CH3

[Markdown] Mermaid class diagram