멀티스레딩
프로그램, 프로세스, 스레드
- 프로그램(디스크 등의 저장소)
- 코드 + 데이터
- 프로세스(RAM)
- 프로그램이 실행되어 활동하는 상태
- 코드 + 데이터 : 프로세스 메모리에 로딩
- 스택: 현재 실행 중인 함수들의 호출 기록과 사용 중인 로컬 변수들
- 힙: 동적 할당
- 멀티프로세싱
- 프로세스가 여러개 실행
- 각 프로세스는 독립된 메모리 공간이 있음
- 스레드
- 프로세스처럼 명령어를 한 줄씩 실행하는 기본 단위
- 프로세스와의 차이점은 아래와 같음.
- 스레드는 한 프로세스 안에 여러 개가 있음.
- 한 프로세스 안에 있는 스레드는 프로세스 안에 있는 메모리 공간(힙)을 같이 사용할 수 있음
- 스레드마다 스택을 가짐. (각 스레드에서 실행되는 함수의 로컬 변수들이 스레드마다 있음)
- 프로그램이 실행되는 기본 단위를 하나의 스레드라고 말할 수 있음.
- 유일한 스레드를 메인 스레드라고 함
- 호출 스택
- 함수 실행이 끝나면 자신을 호출한 지점으로 되돌아가야함. 이 정보가 저장되는 곳이 호출 스택
- 함수 안에 선언된 지역 변수 또한 스택에 있음.
- 각 스레드는 실행 지점이 서로 다름.
- 따라서 각 스레드는 각자 호출 스택을 가짐.
스레드 일생
- 메인 스레드가 먼저 종료되면, 다른 스레드가 아직 실행중이므로 프로세스는 종료되지 않는다. (좀비 프로세스)
멀티스레드 프로그래밍은 언제?
- 오래 걸리는 일 하나와 빨리 끝나는 일을 같이 수행해야 할 때
- 로딩
- 그래픽 리소스 로딩 등..
- 로딩 진행상황 그래프 or 애니메이션 or 미니게임
- 로딩
- 어떤 긴 처리를 진행하는 동안 다른 짧은 일을 처리해야 할 때
- 디스크 액세스하는 스레드는 디스크의 처리 결과가 끝날 때까지 기다려야하는데, 이 시간 동안 CPU는 놀고 있음
- 이 노는 시간을 분배하여 실행 성능을 개선
- 디스크 액세스하는 스레드는 디스크의 처리 결과가 끝날 때까지 기다려야하는데, 이 시간 동안 CPU는 놀고 있음
- CPU를 모두 활용해야 할 때
- 늘어나는 CPU 코어 개수
스레드 정체
- 컨텍스트 스위치: 각 스레드를 실행하다 말고 다른 스레드를 마저 실행하는 과정
- 비용이 듦: 스레드의 상태(스택 등)을 저장, 다른 스레드 선택, 스레드 복원, 실행하던 지점으로 이동
- 빈도를 줄여도 문제: 끊김 현상
- 기본적으로 컨텍스트 스위치 실행은 사람이 쾌적하게 느끼는 시간 단위(타임 슬라이스)로 이루어짐
- os, cpu 등 환경마다 다르지만, 대략 5밀리초
- CPU 개수와 스레드 개수(비둘기 집의 원리: 비둘기 집 = cpu, 비둘기 = 스레드)
- 같으면 컨텍스트 스위치가 발생할 이유가 없음
- 스레드가 더 많으면 발생함
- 실행 중 상태인 스레드 > CPU: 성능 문제
- 주의점
- 프로그램 어느 지점에서 컨텍스트 스위치가 일어나는지
- 기계어 명령어를 다 실행하고 난 후, 컨텍스트 스위치가 일어남 (데이터 레이스 문제 발생 가능성)
- 즉, 기계어 명령 단위로 이루어짐
- 프로그램 어느 지점에서 컨텍스트 스위치가 일어나는지
스레드 주의사항
- 데이터 레이스(경쟁 상태)
- 여러 스레드가 데이터에 접근하여 그 상태를 예측할 수 없게 하는 것
- 해결방법: 원자성(atomicity)을 만족하는 코드 작성
- 항상 변수는 일관성 있는 상태를 유지해야함 (consistency)
- ‘동기화’라고 부르며, 임계영역과 뮤텍스(상호배제), lock 기법 을 사용
임계 영역과 뮤텍스
- 다른 스레드가 건드리지 못하게 하는 뮤텍스(상호 배제: mutual exclusion)
- X, Y를 보호하는 뮤텍스 MX를 생성
- 스레드는 X, Y를 건드리기 전에 MX에 사용권을 요청해야함
- 사용권을 얻고 스레드가 X, Y에 접근
- 접근이 끝나면 MX에 사용권 해제 요청
- 뮤텍스 lock 과 unlock 구간을 지정함으로 써 수행됨
- 이러한 뮤텍스를 임계 영역(critical section)이라고 부름.
- 윈도: 뮤텍스 와 임계영역… 임계 영역이 처리 속도 빠름
- 임계영역: 리눅스에서 unnamed 뮤텍스에 해당
- 리눅스: 뮤텍스… 뮤텍스는 임계 영역과 같은 방식으로 작동할 수 있도록 최적화됨
- 윈도: 뮤텍스 와 임계영역… 임계 영역이 처리 속도 빠름
- 이러한 lock 과 unlock은 예외처리가 힘듦
- 구간 중간에 예외가 발생해도 unlock()을 실행할 수 있도록 해야함
- 해결방법: lock_guard
- 소멸시 자동으로 잠금해제
- C#에서는 lock 구문 블록을 이용하면 자동으로 잠금해제됨
- recursive_mutex
- 상호배타적 + 재귀적인 소유권
- 소유권은 스레드가 lock한 횟수만큼 unlock되어야 해제
- 최대 lock 가능 횟수를 넘어가면 시스템 에러 예외 발생
- https://stackoverflow.com/questions/2415082/when-to-use-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)
- 시리얼 병목이 있을 때 CPU 개수가 많을수록 총 처리 효율성이 떨어지는 현상
- CUP 타임과 디바이스 타임
- 디바이스: 네트워크 인터페이스, 디스크 등 뭔가 요청해서 결과를 기다리는 시간을 의미
- 스레드는 잠자는 상태 (sleep)
- 디바이스: 네트워크 인터페이스, 디스크 등 뭔가 요청해서 결과를 기다리는 시간을 의미
싱글스레드 게임 서버
CPU 개수만큼 프로세스를 띄움(싱글 스레드 멀티 프로세스)
- 디스크에서 플레이어 정보를 로딩할 때 발생하는 디바이스 타임을 처리하는 과정에서 큰 시리얼 병목이 일어남
- 코루틴이나 비동기함수를 사용
- 방 개수만큼 스레드나 프로세스가 있으면 스레드나 프로세스 간 컨텍스트 스위치 횟수가 증가
- 동시접속자 수 크게 떨어짐
멀티스레드 게임 서버
- 서버 프로세스를 많이 띄우기 곤란할 경우
- 프로세스당 로딩해야 하는 게임 정보(맵 등)의 용량이 클 때(mmo 서버)
- 서버 한 대의 프로세스가 여러 CPU의 연산량을 동원해야 할 만큼 많은 연산을 할 때
- 코루틴이나 비동기 함수를 쓸 수 없고 디바이스 타임이 발생할 때
- 서버 인스턴스를 서버 기기당 하나만 두어야 할 때
- 서로 다른 방이 같은 메모리 공간을 액세스해야 할 때
- 잠금 범위를 설정해주어야 하는데 보통은 방 단위로 잠금 범위를 설정
클래스 표현
- 게임 서버 메인의 데이터 구조는 클래스 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; // 서버 메인과 방목록을 위한
};
- 플레이어 행동 처리는 각 방을 잠근 후에 함
- 공통 데이터(방목록 등)을 잠금
- 플레이어 A가 들어 있는 방을 방 목록에서 찾음
- 공통 데이터를 잠금 해제함.
- 찾은 방을 잠금
- 플레이어 A의 방 안에서 처리를 함
- 방을 잠금 해제함
- 가장 큰 주의할 점은 시리얼 병목과 교착 상태
- 파일 접근은 특히 주의(뮤텍스 잠근 채로 접근하는 경우 성능 저하 발생)
스레드 풀링
이벤트
세마포어
원자조작
멀티스레드 프로그래밍의 흔한 실수들
출처
게임 서버 프로그래밍 교과서