프로그램
- 컴퓨터에서 실행되는 명령어 모음이며, 데이터 덩어리이다. 크게 코드와 데이터로 구성되어 있다.
- 디스크에 존재한다.
프로세스
- 프로그램 안에 들어 있는 명령어가 한 줄씩 실행되면서 프로그램이 활동을 하는 상태
- 프로세스 메모리에는 프로그램의 코드와 데이터, 힙, 스택이 존재한다.
- RAM 메모리에 존재한다.
- 각 프로세스에는 독립된 메모리 공간이 존재한다.
- 메인 스레드가 종료되었지만, 다른 스레드가 활동하며 프로세스가 종료되지 않고 계속 남아있는 상태를 좀비 프로세스라고 함
스레드
- 한 프로세스안에 스레드는 여러개 존재할 수 있다.
- 같은 프로세스 안에 있는 스레드는 메모리 공간을 같이 사용할 수 있다.(힙 공간은 공유)
- 스레드마다 스택(호출 스택 포함)을 가진다. (함수의 로컬 변수들이 스레드마다 존재함을 알면 이해가 쉬움)
- 하나의 스레드만 실행되는 프로그램을 설계하고 구현하는 것을 싱글스레드 모델
- 여러 스레드가 동시에 실행되는 프로그램을 설계하고 구현하는 것을 멀티스레드 모델 또는 멀티스레딩
- CPU 코어 한 개의 성능만큼만 사용한다.
- 여러 프로세스와 여러 스레드를 동시에 실행해야 하는 운영체제는 스레드들을 일정 시간마다
번갈아 가면서 실행함 이를 컨텍스트 스위치라고 함
- CPU 갯수보다 스레드(Runnable 상태)갯수가 많을 경우 반드시 CPU에서 컨텍스트 스위치가 발생함.
- 두 스레드가 데이터에 접근해서 해당 데이터의 상태를 예측할 수 없게 하는 것을 경쟁 상태 혹은 데이터 레이스라고 함
- 두 스레드가 한 데이터를 액세스하려고 하는 상황을 컨텐션(Contention) 이라고 함.
- 두 스레드가 서로를 기다리는 상황을 교착 상태(Dead lock) 이라고 함.
- 여러 CPU가 각 스레드의 연산을 실행하여 동시 처리량을 올리는 것을 병렬성(Parallelism) 이라고 함.
멀티스레드가 필요한 상황
- 오래 걸리는 일 하나와 빨리 끝나는 일 여럿을 같이 해야 할 때
- 긴 처리를 진행하는 동안 다른 짧은 일을 처리해야 할 때
- 모든 CPU를 활용해야 할 때
데이터 레이스(경쟁 상태) 해결 방법
- (원자성) A 스레드가 데이터에 접근하고 있을 때 B 스레드가 접근하지 못하도록 함
- (일관성) 데이터의 멤버 변수를 모두 바꾸던지, 하나도 안바꾸던지 하는 항상 일관성 있는 상태를 유지할 수 있도록 함
- 원자성과 일관성을 유지하는 조치들을 통칭하여 동기화라고 하며, 이를 사용하여 데이터 레이스를 해결할 수 있다.
동기화 종류
- 임계 영역
- 뮤텍스(상호 배제)
- 한 스레드가 뮤텍스를 여러번 반복해서 잠그는 것을 원할하게 처리해주는 것을 재귀 뮤텍스(recusive_mutex) 라고 함.
- 잠금(lock)
뮤텍스 작업 예시
- 설명) read 함수 또는 write 함수 중 예외를 던지면 unlock이 실행되지 못하기 때문에 x는 프로그램이 종료되지 않는 한 락 상태로 유지되게 되는 이슈가 발생함. 따라서 모든 로컬 함수에 try - catch 문을 감싸서 예외 발생 시 unlock이 실행되도록 처리하거나, C++ 에서는 lock_guard을 사용한다.
std::mutex mx;
mx.lock();
read(x);
write(x);
mx.unlock();
- 설명) C++ 에서는 뮤텍스 잠금 상태를 저장하는 로컬 변수가 사라질 때 자동으로 잠금 해제가 가능한 lock_guard 클래스를 지원한다.(lock 객체가 사라질 때(즉 해당 스코프에서 나올 때), read 함수 또는 write 함수 중 예외를 던질 때 자동으로 mx.unlock()이 실행됨)
std::recursive_mutex mx;
{
lock_guard<recursive_mutex> lock(mx);
read(x);
write(x);
}
- 설명) 재귀 뮤텍스 B, C가 두 번씩 잠겨도 unlock은 한 번씩 호출해도 잠금은 해제됨. 중복 잠금일 경우 잠금 순서와 달라도 교착 상태를 일으키지 않는다.( A -> B -> C 잠금 순서라고 가정)
lock(A)
lock(B)
lock(C)
lock(B)
lock(A)
unlock(A)
unlock(B)
unlock(C)
멀티스레드로 설계하여 모든 CPU를 사용하더라도 스레드의 갯수 만큼 효율이 좋아지지 않는 이유
- 한 스레드가 락을 하고 있어서 다른 스레드가 대기 상태로 전환되는 상황이 발생한다.
- CPU가 메모리에 접근하는데 시간이 걸림, 이를 메모리 바운드 시간이라고 함
- CPU안에 캐시 메모리가 있어서 일정 부분 효율을 높일 수 있으나, 여러 CPU가 캐시 메모리에 접근할 경우 블록킹이 발생함.
뮤텍스를 최대한 잘게 나누어 설계하였을 경우에 단점
- 프로그램 성능이 떨어진다. 뮤텍스를 액세스하는 과정 자체가 무겁다.
- 프로그램이 복잡해진다. 교착 상태(dead lock) 문제가 쉽게 발생한다.
뮤텍스가 여럿일 때 교착 상태를 일으키지 않는 방법
시리얼 병목(Seial bottleneck)이 발생하는 이유
- 뮤텍스로 보호하는 영역이 넓고, 다른 스레드들이 접근하고자 하는 데이터가 포함되어 있다면, 병렬로 실행되도록 프로그램을 만들었어도 한 CPU만 연산을 수행하는 시리얼 병목 현상이 발생함.
- 디바이스 타임 또는 CPU 타임에 의해 발생. 이때는 CPU가 연산을 하지 않고 시간이 낭비됨.
네트워크 인터페이스, 디스크, 데이터 베이스 등에 요청해서 결과가 올 때 까지 기다리는 시간을 디바이스 타임이라고 함.
- 시리얼 병목이 있는 상황에서 CPU 개수가 많을수록 총 처리 효율성이 떨어지는 현상을 암달의 법칙 또는 암달의 저주라고 함
시리얼 병목(Seial bottleneck)을 해결하는 방법
- 싱글스레드 서버에서 비동기 함수나 코루틴을 사용한다.
싱글스레드 게임 서버를 설계하는 이유
- CPU 갯수만큼 프로세스를 띄우는 구조가 필요할 때 사용
- 멀티스레드 프로그래밍보다 유지 보수와 작업에 적응이 빠름
멀티스레드 게임 서버를 설계하는 이유
- 서버에 프로세스를 많이 띄우지 못할 때, 프로세스에 데이터의 용량이 클 때 사용(MMO 서버)
- 서버 한 대의 프로세스가 여러 CPU의 연산량을 동원해야 할만큼 많은 연산을 필요로 할 때 사용
- 코루틴이나 비동기 함수를 쓸 수 없고 디바이스 타임이 발생할 때 사용
- 서버 인스턴스를 서버 기기당 하나만 두어야 할 때 사용
- 서로 다른 스레드 및 데이터가 같은 메모리 공간을 엑세스해야 할 때 사용
스레드 풀링에서 스레드 갯수를 알맞게 도출하는 방법
- 서버의 주 역할이 디바이스 타임 없이 CPU 연산만하는 스레드라면, 스레드 풀의 스레드 갯수는 서버의 CPU 갯수와 동일하게 잡아도 충분하다.(CPU 갯수보다 스레드 갯수가 더 많을 경우 컨텍스트 스위치가 발생하며, 이는 서버에 불필요한 CPU 연산을 야기시킨다.)
- 서버에서 디바이스 타임이 발생하는 작업이 있을 경우, 스레드 갯수는 CPU 갯수보다 많아야 한다.(잠자는 스레드를 대신해 다른 스레드의 연산을 진행할 수 있다.)
이벤트(Event)
- 스레드 간 소통하며 일을 처리해야할 때 유용하다.
- 기다리는 스레드를 깨우는 도구로 사용되며, Reset = 0, Set = 1 의 상태를 가진다.
- 윈도우에서 이벤트는 자동 이벤트 모드, 수동 이벤트 모드를 취한다.
- 자동 이벤트 모드는 신호를 가질 때(상태 값 = 1) 이벤트를 기다리고 있는 스레드가 있으면 한 개만 깨우고 자동으로 다시 재운다(상태 값 = 0)
- 수동 이벤트 모드는 신호를 가질 때(상태 값 = 1) 수동으로 재우지 않으면 상태 값이 1로 유지된다.
맥박(pulse) 기능을 활용하면, 모든 기다리고 있는 스레드를 1회만 깨우고 다시 재운다(상태 값 = 0)
세마포어(Semaphore)
- 원하는 갯수의 스레드가 자원을 액세스할 수 있게 한다.
- 상태 값은 액세스 가능한 스레드의 갯수가 되며, 0 이상의 정수이다.
원자 조작(Automic operation)
- 뮤텍스나 임계 영역 잠금 없이도 여러 스레드가 안전하게 접근할 수 있다.
- 32, 64비트의 변수 타입에 여러 스레드가 접근 할 때 한 스레드씩만 처리됨을 보장한다.
- 설명) a 가 어떤 스레드에 의해 변경될 수 있지만, a에 3을 더하고 결과를 받고 싶다.
volatile int a = 0;
int b = AtomicAdd(&a, 3);
액터 모델(Actor model)
- 싱글스레드 프로세스가 다른 프로세스와 메시지를 주고 받으면서 병렬 처리를 하는 모델을 의미
- 아카(Akka) 나 스파크(Spark) 등이 액터 모델을 구현한 프레임 워크