눈팅하는 게임개발자 블로그
스레드 동기화 본문
멀티스레딩 환경에서 여러 스레드가 동시에 공용 데이터에 접근하는 경우 공용 데이터가 훼손되는 문제가 발생한다.
이를 해결하기 위해 스레드들을 동기화할 필요가 있다.
스레드 동기화
공용 데이터에 접근하고자 하는 다수의 스레드가 충돌 없이
데이터에 접근하기 위해 상호 협력하는 상황을 만드는 것.
한 스레드가 공용 데이터에 배타적 독점적으로 접근한다.
임계 구역과 상호 배제 개념
- 경쟁
여러 스레드가 공유 데이터 혹은 공유 데이터를 액세스하는 임계 구역에 동시에 접근하는 상황
- 임계 구역(critical section)
공유 데이터에 접근하는 프로그램 코드들(공유 데이터, 테이블 갱신, 파일 쓰기 등)
- 상호 배제(mutual exclusion)
임계 구역에 대한 경쟁 상황의 해결책.
임계 구역에 오직 한 스레드만 접근할 수 있도록 하는 기법.
임계 구역에 먼저 진입한 스레드가 임계 구역 코드의 실행을 끝낼 때까지 다른 스레드가 진입하지 못하도록 보장한다.
여러 스레드가 공유 데이터에 동시에 접근하지 못하도록 임계 구역을 설정하여 임계 구역에 진입한 스레드만
공유 데이터에 접근할 수 있도록 상호 배제하여 경쟁이 일어나지 않도록 스레드를 동기화 하는 것이 기본 목적.
상호 배제(mutual exclusion)
상호 배제를 포함하는 프로그램 구조 : 4가지 구성 요소로 분할
1. 일반 코드(non-critical section)
공유 데이터에 액세스하지 않는 일반 코드.
2. 임계 구역 진입 코드(entry code) - 커널의 도움을 받는다
스레드가 임계 구역에 들어가 있는지 검사, 아무도 없다면 다른 스레드가 들어오지 못하도록 조치.
이미 임계 구역에 진입한 코드가 있다면
진입이 가능할 때까지 쉬지 않고 계속 체크하거나 대기 큐에서 대기하면서 임계 구역을 빠져 나오는
스레드가 큐에서 대기중인 스레드를 깨워 다시 진입 여부를 체크.
3. 임계 구역 코드(critical section)
공유 데이터에 액세스하는 코드.
4. 임계 구역 진출 코드(exit code) - 커널의 도움을 받는다
임계 구역에서 벗어나며 다른 스레드가 임계 구역에 진입할 수 있도록 조치하는 코드.
상호 배제를 구현하기 위한 방법으로 오늘날 대부분은 하드웨어적 솔루션을 사용한다.
1. 인터럽트 서비스 금지
인터럽트가 발생해도 CPU가 인터럽트를 무시하도록 하는 명령어를 이용.
CPU 내에 존재하는 Interrupt Fag가 0이되어 발생하는 인터럽트들을 무시하도록 한다.
스레드가 임계 구역내에 들어가면 인터럽트에 의해 중단되는 일이 없도록 만듬.
따라서 임계 구역 내의 스레드는 선점될 일이 없음.(자신의 일이 끝나기 전에 중단되지 않음)
문제점 : CPU가 모든 인터럽트를 무시하게 되는 상황자체가 문제가 된다.(굉장히 중요한 인터럽트를 무시하게 됨)
또한 멀티코어 CPU 시스템에서는 활용이 불가능하다.
2. 단순 lock 변수로 상호 배제 시도.
Lock/Unlock 방식으로 임계 구역의 entry/exit 코드를 작성.
lock 값을 읽어내어 1로 바꾸는 것으로 lock 권한을 가져간다고 할 때.
Lock 값을 읽어온 타이밍(lock 값을 1로 세팅하기 직전)에
컨텍스트 스위칭이 발생하면 lock 값이 0인 상태로두 개의 스레드가 lock값을 0으로 읽어버린다.
3. 원자 기계어 명령(atomic instruction) 사용
상호 배제 구현에 가장 많이 사용되는 방법.
위의 문제에서 lock 값을 읽는 것과 lock 값을 변경하는 것을 한 줄로 동시에 처리.
임계 구역의 entry / exit 코드를 원자 명령으로 재작성.
멀티스레드 동기화 기법
대표적인 기법들로 Lock 방식, Wait-Signal 방식이 있다.
Lock 방식 : 뮤텍스(mutex), 스핀락(spinlock)
임계 구역에 대한 접근 권한을 lock 변수로 지정해 한 스레드만이 접근할 수 있도록 함.
상호 배제가 되도록 만들어진 lock 변수를 활용.
lock을 소유한 스레드만이 임계 구역에 진입.
lock을 소유하지 않은 스레드가 진입을 시도할 때 lock의 소유권을 가진 스레드가 이를 놓을 때까지 대기.
Mutex POSIX 표준 라이브러리
pthread_mutex_t lock; // 뮤텍스 락 변수 선언.
pthread_mutex_init(&lock, NULL); // 뮤텍스 락 변수 lock 초기화
pthread_mutex_lock(&lock); // entry code, 뮤텍스 락 잠그기
pthread_mutex_unlock(&lock); // exit code. 뮤텍스 락 열기
spinlock POSIX 표준 라이브러리
pthread_spinlock_t lock; //스핀락 변수 선언
pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE); // 한 프로세스에 속한 스레드만이 공유하는 변수로 선언.
pthread_spin_lock(&lock); // entry code
pthread_spin_unlock(&lock); // exit code
Wait-Signal 방식 : 세마포어(semaphore)
n개의 자원에 대한 m개의 멀티스레드 사이의 원활한 공유.
n개의 자원 중 활용 가능한 자원을 관리하며 요청이 들어올 시 해당 스레드에 할당해줌.
n개의 자원이 모두 활용 중이라면 사용 가능한 자원이 생길 때까지 스레드는 대기.
공중화장실을 이용하는 방식과 같은 개념이다.
자원을 할당받지 못한 경우의 행동에 따른 구분.
Sleep-wait 세마포어 P연산 : 대기 큐에서 잠자기, V연산 : 사용 가능 자원이 있으면 잠자는 스레드 깨우기.
Busy-wait 세마포어 P연산 : 사용 가능자원이 생길 때까지 무한 루프, V연산 : counter++
세마포어 POSIX 표준 라이브러리
sem_t sem; // POSIX의 세마포어 구조체
sem_init(&sem, NO(0), MAX_COUNTER(3)); // 세마포어 초기화