Critical Section
임계 구역(Critical Section): 여러 프로세스 또는 스레드가 공유 자원에 접근하는 코드 구간 중에 동시에 실행되면 데이터 불일치가 발생할 수 있는 영역.
Critical Section 보호
여러 실행 흐름이 같은 변수, 파일, 자료구조, 장치 상태 등에 동시에 접근하면 Race Condition이 발생할 수 있다.
이때 Critical Section 앞뒤에 Lock, Mutex, Semaphore, Spinlock 같은 동기화 기법을 배치해서 한 번에 하나의 실행 흐름만 공유 자원에 접근하도록 만들어야한다.
int counter = 0;
counter++;
위의 C언어 코드는 단순히 counter 값을 1 증가시키는 코드처럼 보이는데 실제로는 메모리에서 값을 읽고, 레지스터에서 값을 증가시키고, 다시 메모리에 저장하는 여러 단계로 나눌수있다. globalvar++ 같은 연산도 어셈블리 레벨에서는 load, add, store 형태의 여러 명령어로 나뉘고 여러 스레드가 이 명령어 사이에서 교차 실행되면 Race Condition이 발생할 수 있다.
대략 다음과 같은 형태가 된다.
mov eax, DWORD PTR [counter]
add eax, 1
mov DWORD PTR [counter], eax
즉, counter++ 는 원자적인 연산이 아니다.
두 개의 스레드가 동시에 counter++ 를 실행하면 다음과 같은 상황이 발생할 수 있다.
순서 Thread A Thread B counter
| 1 | counter 값 읽기 | 0 | |
| 2 | counter 값 읽기 | 0 | |
| 3 | 0 + 1 계산 | 0 | |
| 4 | 0 + 1 계산 | 0 | |
| 5 | 1 저장 | 1 | |
| 6 | 1 저장 | 1 |
원래는 두 스레드가 각각 1씩 증가시켰기 때문에 결과가 2가 되어야 한다.
하지만 두 스레드가 같은 기존 값 0을 읽고 각각 1을 저장했기 때문에 최종 결과는 1이 된다.
이런 문제가 Race Condition이다.
Critical Section 구조
Critical Section은 일반적으로 다음과 같은 구조로 표현된다.
do {
entry section
critical section
exit section
remainder section
} while (1);
각 영역의 의미는 다음과 같다.
영역 설명
| Entry Section | Critical Section에 들어가기 전에 권한을 얻는 영역 |
| Critical Section | 공유 자원에 접근하는 영역 |
| Exit Section | Critical Section 사용이 끝난 뒤 권한을 반납하는 영역 |
| Remainder Section | 공유 자원과 직접 관련 없는 나머지 코드 영역 |
Critical Section 사용시 주의점
Critical Section은 가능한한 짧게 유지해야 한다.
Critical Section 안에서 시간이 오래 걸리는 작업을 수행하면 다른 스레드들이 모두 대기하게 된다.
특히 파일 I/O, 네트워크 I/O, 긴 연산, 사용자 입력 대기 같은 작업을 Lock을 잡은 상태에서 수행하면 전체 성능이 크게 떨어질 수 있다.
pthread_mutex_lock(&lock);
update_shared_state();
pthread_mutex_unlock(&lock);
do_slow_work();
위와 같이 공유 상태를 갱신하는 부분만 Critical Section에 넣고, 오래 걸리는 작업은 Lock 밖에서 수행하는 것이 좋다.
반대로 다음과 같은 코드는 좋지 않다.
pthread_mutex_lock(&lock);
update_shared_state();
do_slow_work();
pthread_mutex_unlock(&lock);
do_slow_work() 가 오래 걸리면 다른 스레드들이 그동안 전부 대기하게 된다.
Critical Section 설계 원칙
Critical Section을 설계할 때는 다음과 같은 점을 고려해야 한다.
원칙 설명
| 최소 범위 | 공유 자원에 접근하는 코드만 Critical Section에 넣는다 |
| 짧은 실행 시간 | Lock을 잡은 상태에서 오래 걸리는 작업을 하지 않는다 |
| 일관된 Lock 순서 | 여러 Lock을 사용할 때 항상 같은 순서로 획득한다 |
| Unlock 보장 | 오류가 발생해도 Lock이 해제되도록 작성한다 |
| 적절한 동기화 객체 선택 | 상황에 따라 Mutex, Spinlock, Semaphore, Atomic Operation을 구분해서 사용한다 |
| 공유 자원 기준 설계 | 코드 위치가 아니라 보호해야 하는 데이터 기준으로 Lock을 설계한다 |
Critical Section은 단순히 코드에 Lock을 붙이는 문제가 아니다.
어떤 데이터를 보호할지, 어디까지를 보호할지, Lock을 얼마나 오래 잡을지, 여러 Lock 사이의 순서를 어떻게 둘지까지 함께 설계해야 한다.
Critical Section의 한계
Critical Section을 사용한다고 해서 모든 동시성 문제가 자동으로 해결되는 것은 아니다.
한계 설명
| Deadlock | Lock 획득 순서가 꼬이면 서로 대기하면서 멈출 수 있음 |
| Starvation | 특정 스레드가 계속 Lock을 얻지 못할 수 있음 |
| Priority Inversion | 낮은 우선순위 스레드가 Lock을 쥐고 있어 높은 우선순위 스레드가 대기할 수 있음 |
| Lock Contention | 여러 스레드가 같은 Lock을 자주 요청하면 성능이 떨어짐 |
| Over-serialization | 너무 큰 범위를 Lock으로 묶으면 병렬성이 줄어듦 |
| Missing Unlock | 예외나 오류 경로에서 Unlock을 하지 않으면 전체가 멈출 수 있음 |
즉, Critical Section은 Race Condition을 막기 위한 핵심 개념이지만, 잘못 사용하면 성능 문제나 교착 상태를 만들 수 있다.
'DevSec' 카테고리의 다른 글
| BPF (0) | 2026.06.10 |
|---|---|
| Mutual Exclusion (0) | 2026.06.06 |
| Busy Waiting (0) | 2026.06.05 |
| Context Switching (0) | 2026.06.05 |
| Spinlock and Mutex (0) | 2026.06.05 |