공유 자원 (Shared Resource) — 여러 스레드가 동시에 사용하는 단일 자원. 동시 접근을 방치하면 결과가 스케줄링 타이밍에 의존하는 경쟁 상태(race condition)가 발생한다. 이를 막으려면 공유 자원을 만지는 코드 구간(임계 영역, critical section)을 상호 배제(mutual exclusion)로 보호해야 한다.
공유 자원의 예
- 전역/정적 변수
- 파일
- 공유 메모리 영역
- 하드웨어 레지스터
이들은 모두 race condition의 원인이 된다. 현실 세계 비유로는 여러 사용자가 쓰는 화장실 칸 — 문을 잠그고 확인한 뒤 들어가는 규약이 필요하다.
경쟁 상태 (Race Condition)
결과가 통제 불가능한 이벤트의 순서·타이밍에 좌우되는 상태.
“통제 불가능한 이벤트”의 대표가 스케줄링 이벤트(선점) 다. 스레드 두 개가 counter = counter + 1;을 각각 실행할 때, 이 한 줄은 기계 명령 3개로 분해된다.
mov 0x8049a1c, %eax ; 로드
add $0x1, %eax ; +1
mov %eax, 0x8049a1c ; 스토어정상 시퀀스 (두 스레드가 순차 완료)는 기대대로 +2가 된다. 그러나 Thread 1이 로드·증가만 한 상태에서 Thread 2에 선점되면, Thread 2가 같은 값을 로드해서 증가·저장한 뒤 Thread 1이 이어서 자신의 값을 저장 — 결과는 +1이다. 기능적으로 틀리고 타이밍에 따라 예측 불가능하다.
임계 영역과 상호 배제
- 임계 영역 (Critical Section): 공유 자원을 다루는 코드 구간. 하나의 공유 자원은 이를 사용하는 서로 다른 스레드에 각각 임계 영역을 만든다.
- 상호 배제 (Mutual Exclusion): 임계 영역에는 한 번에 한 스레드만 진입.
- 원자성 (Atomicity):
counter = counter + 1;은 원자적이지 않다. 3개 명령으로 분리되므로 중간에 선점 가능하기 때문.
보호 방식 비교
원천적 접근 3가지. 뒤로 갈수록 실용적이다.
#1. 스케줄링 비활성화
임계 영역 직전에 스케줄링을 끄고, 직후에 다시 켠다.
void *mythread(void *arg) {
for (i = 0; i < max; i++) {
disable_scheduling();
counter = counter + 1; // Critical Section
enable_scheduling();
}
return NULL;
}문제점:
- 일반 프로세스는 스케줄링을 제어할 수 없음 → 특권 필요
- 악성 프로그램이 스케줄링을 잠그면 시스템 전체가 마비
- 인터럽트 핸들러가 같은 공유 자원을 만지는 경우 보호 불가
#2. 인터럽트 비활성화
타이머 인터럽트를 끄면 자연스럽게 스케줄링도 멈춘다.
disable_interrupts();
counter = counter + 1;
enable_interrupts();문제점: 프로세스가 외부 이벤트에 반응할 수 없다.
#3. 락 (Mutex)
스레드 사이의 약속 — 락을 쥔 스레드만 진입. 뮤텍스와 락에서 상세.
lock_t mutex;
void mythread(void *arg) {
for (i = 0; i < max; i++) {
lock(&mutex);
counter = counter + 1;
unlock(&mutex);
}
return NULL;
}방식 #1·#2의 한계 (특권 필요·외부 이벤트 차단)를 회피하는 실용적 해법. 다만 락 자체가 새로운 문제(데드락, 우선순위 역전)를 불러온다.