[게임 서버] 1.16 멀티스레드 프로그래밍의 흔한 실수들

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
게임 서버 프로그래밍 교과서, 배현직 저자

📦 1. 멀티스레딩

👉🏻 항목 16: 멀티스레드 프로그래밍의 흔한 실수들

1. 읽기와 쓰기 모두에 잠금하지 않음

int a;
mutex a_mutex;

void func1()
{
  // lock(a_mutex); 누락
  print(a);
}
  • 값을 읽을 때도 잠금이 필요하다.
  • 쓰기만 보호하면 안전하다고 착각하기 쉬운데, 읽는 중에 다른 스레드가 수정하면 위험하다.

2. 잠금 순서 꼬임

void func1()
{
  lock(a_mutex);
  ...
  lock(b_mutex);
}

void func2()
{
  lock(b_mutex);
  ...
  lock(a_mutex);
}
  • 잠금 순서가 뒤바뀌면 교착 상태(deadlock) 발생 가능.
  • 잠금 순서를 항상 일정하게 유지하도록 하자.
  • 프로그램 규모가 커질수록 잠금 순서 규칙을 간단하게 유지하는 것이 중요하다.

3. 너무 좁은 잠금 범위

  • 임계 영역이 너무 작으면 오히려 잠금 횟수가 늘어나고, 유지보수도 힘들어진다.
  • 반대로 너무 넓으면, 컨텍스트 스위치 시 운영체제 부하가 증가한다.
  • 잠금 범위는 짧지만 의미 있는 작업 단위로 최소화하는 것이 좋다.

4. 디바이스 타임이 섞인 잠금

  • 콘솔 출력, 로그 출력 등은 디바이스 I/O로 느리다.
  • 잠금 상태에서 이런 작업을 수행하면 다른 스레드를 불필요하게 대기시킨다.
    출력은 잠금 밖에서 하도록 하자.

5. 잠금의 전염성

lock(list_mutex);
A* a = list.GetFirst();
unlock(list_mutex);  // 여기서 잠금 해제하면 안됐음!

a->x++;              // 문제 발생
  • 잠금으로 보호되는 객체에서 얻어온 포인터나 참조를 사용할 때도,
    해당 사용이 끝날 때까지 잠금을 유지해야 한다.
  • 이것을 잠금의 전염성이라고 한다.

6. 잠금된 뮤텍스나 임계 영역 삭제

class A {
  mutex mutex;
  int a;
};

void func() {
  A* a = new A();
  lock(a->mutex);
  delete a; // 뮤텍스 잠금 해제는 어쩌고..?
}
  • 삭제 직전에 잠금이 풀리지 않으면, 뮤텍스 객체가 살아있는 상태에서 사라진다.
  • 해결 방법: 소멸자에서 잠금 상태인지 검사하여 오류를 내도록 만들자.

7. 일관성 규칙 깨기

class Node
{
  Node* next;
};
 
Node* list = null;
int listCount = 0;
 
mutex listMutex;
mutex listCountMutex;
 
void func()
{
  lock(listMutex);
  Node* newNode = new Node();
  newNode->next = list;
  list = newNode;
  unlock(listMutex);
 
  lock(listCountMutex);
  listCount++;
  unlock(listCountMutex);
}
  • listlistCount논리적으로 하나의 데이터 구조임에도 각각 따로 잠금.
    → 이로 인해 항목 수와 리스트 상태 불일치 가능성 발생.
  • 해결 방법: 두 데이터를 하나의 잠금으로 묶어 보호해야 한다.

✅ 병렬 자료구조와 원자 조작도 예외는 아니다

ParallelQueue<int> queue;      // 병렬 자료 구조
atomic<int> item;              // 원자 조작 변수

void func()
{
  int i = queue.pop();       // 큐에서 꺼낸다.
  item = i;                  // 꺼낸 것을 여기다 넣는다.
}

void func2()
{
  int i = item.exchange(0);  // 꺼냈던 항목을 가져와서 사용한다.
  if (i != 0)
  {
      ...;
  }
}
  • queue.pop()item 저장 사이 짧은 틈에 다른 스레드가 접근하면 일관성이 깨진다.
  • 병렬 자료구조나 atomic 변수도 사용 방식에 따라 일관성이 무너질 수 있다.
    → 이 경우도 잠금으로 묶어 일관성 확보가 필요하다.

🧐 정리

  • 읽기에도 반드시 잠금을 해주자.
  • 잠금 순서를 일정하게 유지하자.
  • 너무 좁은 임계 영역은 오히려 유지보수를 어렵게 만든다.
  • 디바이스 작업은 잠금 밖에서 처리하자.
  • 잠금 전염성 개념을 명확히 이해하고 적용하자.
  • 잠긴 객체를 삭제하지 않도록 주의하자.
  • 서로 연관된 데이터를 분리된 잠금으로 다루지 말자.
  • 병렬 자료구조나 atomic 연산도 일관성 유지를 위한 잠금이 필요할 수 있다.

GameServer 카테고리 내 다른 글 보러가기

댓글남기기