1. 스레드 동기화의 필요성
스레드 동기화의 필요성
- 다수의 스레드가 동시에 공유 데이터에 쓰기를 접근하는 경우에 공유데이터가 훼손되는 문제 발생 가능
- 두 스레드가 동시에 공유 데이터를 읽는 경우 -> 문제 없음
- 한 스레드는 쓰고 한 스레드는 읽을 경우 -> 읽고 쓰는 순서에 따라 읽는 값이 달라질 수 있지만 공유데이터의 훼손은 없음
- 두 스레드가 동시에 공유 데이터에 쓰는 경우 -> 공유 데이터 훼손 가능성
- 스레드 동기화(thread synchronization)
- 공유데이터에 대한 다수의 스레드가 동시에 접근할 때 공유데이터가 훼손되지 않게 하는 기법
- 한 스레드가 공유데이터를 배타적 독점적으로 접근하도록 순서화 하는것
공유 집계판에 동시 접근하는 사례
위 문제를 C언어 코드로 작성하면
#include <stdio.h>
#include <pthread.h>
int sum = 0;
void* worker(void* arg) {
for (int i = 0; i < 1000000; i++) {
sum = sum + 10;
}
}
int main() {
char* name[] = { "박지성", "손흥민" };
pthread_t tid[2];
pthread_attr_t attr[2];
pthread_attr_init(&attr[0]);
pthread_attr_init(&attr[1]);
pthread_create(&tid[0], &attr[0], worker, name[0]);
pthread_create(&tid[1], &attr[1], worker, name[1]);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
printf("sum = %d\n", sum);
return 0;
}
위 코드를 실행하면 아래와 같은 결과가 발생한다.

total sum이 20000000이 되어야 정상인데, 공유 변수 sum에 대한 두 스레드의 충돌로 인해, 20000000이 안되는 문제가 발생했다.
-
공유 데이터 접근 문제의 해결책
- 문제점
- 여러 스레드가 공유 변수에 접근할 때, 공유 데이터 훼손
- 해결책 - 스레드 동기화
- 한 스레드가 공유 데이터 사용을 마칠 때까지,
- 다른 스레드가 공유 데이터에 접근하지 못하도록 제어
- 멀티스레드의 경쟁 상황은 매우 자주 발생
- 사용자의 멀티스레드 프로그램에서 자주 발생
- 커널 코드에서 매우 자주 발생
-> 커널에 공유 데이터가 많기 때문에
- 다중 코에어서는 더욱 조심해야함
-
임계구역과 상호배제
-
임계구역(critical section)
-
상호배제(mutual exclusion)
- 임계구역을 오직 한 스레드만 배타적 독점적으로 사용하도록 하는 기술
- 임계구역에 먼저 진입한 스레드가 임계구역의 실행을 끝낼 때까지,
- 다른 스레드가 진입하지 못하도록 보장

2. 상호 배제(mutual exclusion)
-
상호 배제를 포함하는 전형적인 프로그램 모습
-
일반 코드(non-critical code)
-
임계구역 진입 코드(entry code)
- 임계구역에 진입하지 전 필요한 코드 블록
- 현재 임계구역을 실행 중인 스레드가 있는지 검사
- 없다면, 다른 스레드가 들어오지 못하도록 조치
- 있다면, 진입이 가능해질 때까지 대기
-
임계구역 코드(critical code)
-
임계구역 진출 코드(exit code)
- 임계구역을 마칠 때 필요한 코드 블록
- 대기중인 스레드가 임계구역에 진입할 수 있도록, 진입 코드에서 취한 조치 해제

-
상호배제 구현
- 상호배제 구현 이유
- 임계구역에 오직 1개의 스레드만 진입하게 하기 위해
- 상호배제 구현 방법
- 소프트웨어적 방법 - Peterson's 알고리즘 등
- 알고리즘 수준에서 제시된 것들로 구현 시 여러 문제 노출
- 하드웨어적 방법 - 하드웨어의 도움을 받는 방법
- 인터럽트 서비스 금지, 원자 명령 활용 등
- 오늘날 대부분 하드웨어적 방법 사용
- 하드웨어적 방법
- 방법 1 - 인터럽트 서비스 금지
- 인터럽트 서비스를 금지하거나 허용하는 CPU 명령 사용
- 방법 2 - 원자 명령(atomic instruction) 사용
- 원자 명령은 CPU 명령임
- 오늘날 상호배제 구현에 사용하는 방법
-
상호배제구현 1 - 인터럽트 서비스 금지
-
방법
- entry 코드에서 인터럽트 서비스를 금지하는 명령 실행
- 장치로부터 인터럽트가 발생해도, CPU가 인터럽트 발생을 무시
- 인터럽트가 발생해도 CPU는 인터럽트 서비스 루틴을 실행하지 않음
- 인터럽트를 무시하면 임계구역을 실행하는 스레드가 중단되지 않음
-
문제점
- 모든 인터럽트가 무시되는 문제 발생
- 멀티 코어 CPU나 다중 CPU를 가진 시스템에서 활용 불가
- 한 CPU의 인터럽트 금지로 다른 CPU에게 인터럽트를 금지시킬 수 없음
-> 현재 CPU의 인터럽트 금지가 다른 CPU에게는 영향을 미치지 않음
- 다른 CPU가 타이머 인터럽트 서비스 루틴을 실행하여, 임계구역을 실행중인 스레드를 컨텍스트 스위칭시키고 다른 스레드를 스케줄할 수 있음

-
lock 변수를 이용한 상호배제
- lock 변수 : 1이면 잠금 상태
- lock 변수 : 0이면 열린 상태



-
위 문제를 해결할 수 있는 방법은 원자명령을 도입하는 것이다.
- 원자 명령(atomic instruction)
3. 멀티스레드 동기화 기법
-
멀티스레드 동기화
- 정의
- 상호배제 기반위에, 자원을 사용하려는 여러 스레드들이 자원을 원활히 공유하도록 하는 기법
- 동기화 프리미티브(synchronization primitives)로 부름
- 대표적인 기법
- locks 방식 : 뮤텍스(mutex), 스핀락(spinlock)
- 상호배제가 되도록 만들어진 락(lock) 활용
- 락을 소유한 스레드만이 임계구역 진입
- 락을 소유하지 않은 스레드는 락이 풀릴 때까지 대기
- wait-signal 방식 : 세마포(semaphore)
- n개 자원을 사용하려는 m개 멀티스레드의 원활한 관리
- 자원을 소유하지 못한 스레드는 대기(wait)
- 자원을 다 사용한 스레드는 알림(signal)
-
뮤텍스(뮤텍스 기법)
-
뮤텍스(mutex) 기법
- 잠김/열림 중 한 상태를 가지는 락 변수 이용
- 한 스레드만 임계구역에 진입시킴
- 다른 스레드는 큐에 대기
- sleep-waiting lock 기법
-
뮤텍스 기법의 구성 요소
-
락 변수
- true/false 중 한 값
- true : 락을 잠근다. 락을 소유한다.
- false : 락을 연다. 락을 해제한다.
-
대기 큐
-
연산
- lock 연산(임계구역의 entry 코드)
- 락이 열린 상태이면, 락을 잠그고 임계구역 진입
- 락이 잠김 상태이면, 현재 스레드를 블록 상태로 만들고 대기 큐에 삽입
- unlock 연산(임계구역의 exit 코드)
- lock = false, 락을 열린 상태로 변경
- 대기 큐에서 기다리는 스레드 하나 깨움

-
뮤텍스 기법의 특징
- 임계구역의 실행 시간이 짧은 경우, 비효율적
-
락이 잠겨 있으면(컨텍스트 스위칭되어) 대기 큐에서 대기, 락이 풀리면 다시 (컨텍스트 스위칭되어) 실행
-
락이 잠겨있는 시간보다 스레드가 잠자고 깨는 데 걸리는 시간이 상대적으로 크면 비효율적
-
스핀락(스핀락 기법)
-
스핀락(spinlock) 기법
- busy-waiting lock 기법
- 스레드가 큐에서 대기하지 않고 락이 열릴 때까지 계속 락 변수 검사
- 뮤텍스와 거의 같고 busy-waiting이라는 점에서만 다름
- 대기 큐 없음
- busy-waiting으로 인해 CPU를 게속 소모, CPU가 다른 스레드를 실행할 수 없음
- 락을 소유한 스레드만 자원 배타적 사용, 동기화 기법
-
스핀락 기법의 구성 요소
- 락 변수
- true/false 중 한 값
- true : 락을 잠근다. 락을 소유한다.
- false : 락을 연다. 락을 해제한다.
- 연산
- lock 연산
- 임계구역에 들어갈 때 실행되는 entry 코드
- 락이 잠김 상태면, 락이 풀릴 때까지 무한 루프 돌면서 lock 연산 시도
- 락이 열린 상태면, 락을 잠김 상태로 바꾸고 임계구역 실행
- unlock 연산
-
임계구역을 나올 때 실행하는 exit 코드
-
락을 열림 상태로 변경

-
스핀락 기법의 특징
- 뮤텍스의 non-blocking 모델
- 락이 잠겨 있을 때 블록되지 않고 락이 풀릴 때까지 검사하는 코드 실행
- 단일 CPU(단일 코어)를 가진 운영체제에서 비효율적
- 단일 코어 CPU에서 의미 없는 CPU 시간 낭비
- 스핀락을 검사하는 스레드의 타임 슬라이스가 끝날 때까지 다른 스레드 실행 안 됨
- 락을 소유한 다른 스레드가 실행되어야 락이 풀림
- 멀티 코어에 적합
-
뮤텍스와 스핀락 비교
-
락이 잠기는 시간이 긴(임계구역이 긴) 경우
- 뮤텍스가 효율적
- 락을 얻지 못했을 때, CPU를 다른 스레드에게 양보하는 것이 효율적
-
단일 CPU를 가진 시스템
-
멀티 코어(멀티 CPU)를 가진 시스템
- 스핀락이 효율적
- 임계구역은 보통 짧게 작성되므로, 잠자고 깨는 컨텍스트 스위칭 없이 바로 자원 사용
-
사용자 응용프로그램과 커널 코드
- 각각 뮤텍스와 스핀락이 효율적
- 커널 코드나 인터럽트 서비스 루틴은 빨리 실행되어야 함
- 인터럽트 서비스 루틴 내에서 잠잘 수 없음
-
스핀락을 사용하면 기아 발생 가능
- 스핀락은 무한 경쟁 방식이어서 기아 발생 가능
-
세마포(세마포 기법)
-
세마포(semaphore) 기법
- 멀티스레드 사이의 자원 관리 기법
- n개의 공유 자원을 다수 스레드가 공유하여 사용하도록 돕는 자원 관리 기법
-
세마포 기법의 구성요소
-
자원 : n개
-
대기큐 : 자원을 할당받지 못한 스레드들이 대기하는 큐
-
counter 변수
- 사용 가능한 자원의 개수를 나타내는 정수형 전역 변수
- n으로 초기화
-
P/V 연산
-
P연산(wait 연산) - 자원 요청 시 실행하는 연산(자원 사용 허가)
-
V 연산(signal 연산) - 자원 반환 시 실행하는 연산(자원 사용 종료)

-
자원을 할당받지 못한 경우의 행동에 따라 sleep-wait/busy-wait 세마포로 구분
- sleep-wait 세마포
- P연산 : counter--, 대기 큐에서 잠자기
- V연산 : counter++, 사용가능 자원이 있으면 잠자는 스레드 깨우기
- busy-wait 세마포
- P연산 : 사용 가능 자원이 생길 때까지 무한 루프 후 자원이 생기면 counter--
- V연산 : counter++

출처
명품 운영체제 (저자 황기태)