슬립락: CPU를 양보하는 Block & Wakeup 동기화
슬립락의 Block & Wakeup 동작, 스핀락과의 CPU 사용 차이, 문맥 교환 비용과 적합한 임계 영역을 정리한다.
2026-08-14 · 최초 발행 2025-12-25
락을 기다리는 스레드를 대기 큐로 보낼 때
슬립락(Sleep Lock)은 이미 점유된 락을 요청한 프로세스나 스레드를 휴면 상태로 전환하는 동기화 메커니즘이다. Busy Waiting으로 CPU를 계속 소비하는 대신, Block & Wakeup 방식으로 CPU를 다른 작업에 넘긴다. 대기자는 운영체제 스케줄러의 대기 큐에서 관리되고, 락이 풀리면 다시 실행 가능한 상태가 된다.
뮤텍스와 세마포어의 Block-Wakeup 구현이 대표적인 형태다. 문맥 교환 비용은 발생하지만, 임계 영역이 길다면 CPU를 비워 두지 않는 편이 더 낫다.
Block과 Wakeup이 이어지는 과정
락이 잠겨 있으면 요청 스레드는 대기 큐에 들어간 뒤 block()으로 휴면 상태가 된다. 이 순간 CPU는 다른 프로세스를 실행할 수 있다. 락을 가진 쪽이 unlock하면 대기 큐에서 스레드를 꺼내 wakeup()하고, 깨어난 스레드는 다시 락 획득을 시도한다.
typedef struct {
int locked;
queue_t wait_queue; // 대기 큐
} sleep_lock_t;
void sleep_lock(sleep_lock_t *lock) {
while (lock->locked) {
enqueue(&lock->wait_queue, current_thread);
block(); // 휴면 상태로 전환
// 여기서 CPU 양보, 다른 프로세스 실행
}
lock->locked = 1;
}
void sleep_unlock(sleep_lock_t *lock) {
lock->locked = 0;
if (!is_empty(&lock->wait_queue)) {
thread_t *t = dequeue(&lock->wait_queue);
wakeup(t); // 대기 스레드 깨움
}
}
실행 상태는 Ready에서 Running으로, 락 획득에 실패하면 Blocked로 이동한다. wakeup() 이후에는 다시 Ready 상태에 놓이고 스케줄링을 기다린다.
대기 큐와 스케줄러를 포함해 보면 흐름은 다음과 같다.
스핀락과 갈리는 지점
슬립락은 대기 중 Blocked 상태에 머물며 CPU를 양보한다. 반대로 스핀락은 Running 상태에서 락을 반복 확인하는 Busy Waiting을 수행한다. 이 차이는 응답 속도와 문맥 교환 비용, 전력 효율의 선택으로 이어진다.
| 항목 | 슬립락 (Sleep Lock) | 스핀락 (Spin Lock) |
|---|---|---|
| 대기 방식 | Block & Sleep | Busy Waiting |
| 상태 | Blocked (휴면) | Running (활성) |
| CPU 사용 | 0% (양보) | 100% (점유) |
| 문맥 교환 | 발생 (2회) | 미발생 |
| 스케줄링 | 대기 큐 관리 | 미개입 |
| 응답 속도 | 느림 | 빠름 |
| 적합한 경우 | 긴 임계 영역 | 짧은 임계 영역 |
| 전력 효율 | 높음 | 낮음 |
문맥 교환은 슬립락의 비용이고 CPU 절약은 그 대가로 얻는 이점이다. 스핀락은 문맥 교환이 없지만, 대기하는 동안 CPU가 무용지물이 될 수 있다.
임계 영역이 짧음(< 1000 사이클)이면 스핀락이 유리하다. 1000~10000 사이클 구간은 상황에 따라 달라지며, 김(> 10000 사이클)이라면 슬립락이 유리하다.
CPU를 비우는 대신 드는 비용
대기 스레드를 휴면으로 전환하면 CPU는 다른 작업을 실행할 수 있다. 멀티태스킹 효율과 배터리 사용 측면에서 유리하며, 모바일·임베디드 시스템처럼 전력 제약이 있는 환경에도 맞는다.
스케줄러가 대기자를 관리하므로 우선순위 역전 문제를 완화해 Hang을 막고, 공정한 자원 분배를 기대할 수 있다. 대기 프로세스 수와 무관하게 안정적으로 동작하는 점도 장점이다. 파일 읽기·쓰기나 네트워크 통신 같은 I/O 대기, 수천~수만 사이클이 걸리는 복잡한 계산, 데이터베이스 트랜잭션 처리가 긴 임계 영역의 예다.
다만 Block과 Wakeup에는 각각 레지스터 저장 또는 복원, 상태 변경, 스케줄링이 필요하다.
| 단계 | 작업 | 비용 (사이클) |
|---|---|---|
| Block | 레지스터 저장, 상태 변경, 스케줄링 | ~1000 |
| Wakeup | 레지스터 복원, 상태 변경, 스케줄링 | ~1000 |
| 총 오버헤드 | ~2000 |
락 해제 직후 즉시 실행되지 않을 수 있고, 문맥 교환은 캐시 미스를 늘릴 수 있다. 대기 큐를 관리하는 비용도 함께 고려해야 한다. 실제 작업이 매우 짧다면 이 비용이 임계 영역 자체보다 커진다.
// 비효율적 사용 예시
mutex_lock(&lock); // 문맥 교환 (~1000 사이클)
counter++; // 실제 작업 (1 사이클)
mutex_unlock(&lock); // 문맥 교환 (~1000 사이클)
// 총 ~2001 사이클, 오버헤드 > 99.9%!
사용할 수 있는 컨텍스트와 피해야 할 곳
슬립락은 사용자 공간 애플리케이션, 파일 I/O·네트워크·데이터베이스처럼 긴 임계 영역, 모바일·IoT·임베디드 환경, CPU 효율이 중요한 단일 코어, 많은 프로세스가 경쟁하는 상황에 적합하다.
반대로 스케줄러 내부처럼 재귀 호출 위험이 있는 커널 임계 구역에서는 맞지 않는다. 인터럽트 핸들러처럼 Sleep할 수 없는 컨텍스트, 수십~수백 사이클로 끝나는 짧은 임계 영역, 엄격한 응답 시간 제약이 있는 실시간 요구도 피해야 한다.
POSIX Mutex는 Block & Sleep 방식으로 긴 임계 영역을 보호하는 데 사용할 수 있다.
pthread_mutex_t mutex;
void* thread_func(void* arg) {
pthread_mutex_lock(&mutex); // Block & Sleep
// 긴 Critical Section (I/O, 복잡한 계산)
process_large_data();
pthread_mutex_unlock(&mutex);
return NULL;
}
조건 변수는 생산자-소비자 구조에서 버퍼가 비었을 때 소비자를 휴면 대기시킬 수 있다.
pthread_mutex_lock(&mutex);
while (buffer_empty) {
pthread_cond_wait(&cond, &mutex); // 휴면 대기
}
item = consume_from_buffer();
pthread_mutex_unlock(&mutex);
일부 시스템은 먼저 짧게 스핀하고, 획득하지 못하면 슬립락으로 전환하는 하이브리드 접근을 사용한다.
// 짧은 시간 스핀 후 슬립
void adaptive_lock(lock_t *lock) {
int spin_count = 0;
const int MAX_SPIN = 100;
// 먼저 스핀 시도
while (spin_count < MAX_SPIN) {
if (try_lock(lock))
return;
spin_count++;
}
// 실패 시 슬립
sleep_lock(lock);
}