[게임 서버] 1.16 멀티스레드 프로그래밍의 흔한 실수들
카테고리: GameServer
태그: GameServer
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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);
}
list
와listCount
는 논리적으로 하나의 데이터 구조임에도 각각 따로 잠금.
→ 이로 인해 항목 수와 리스트 상태 불일치 가능성 발생.- 해결 방법: 두 데이터를 하나의 잠금으로 묶어 보호해야 한다.
✅ 병렬 자료구조와 원자 조작도 예외는 아니다
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 연산도 일관성 유지를 위한 잠금이 필요할 수 있다.
댓글남기기