스핀락과 Busy Waiting: 짧은 임계 영역을 위한 커널 동기화
스핀락의 Busy Waiting 동작, 문맥 교환과 CPU 사용 특성, 세마포어와의 차이 및 커널 사용 조건을 정리한다.
2026-08-14 · 최초 발행 2025-12-25
락이 풀릴 때까지 CPU를 쓰며 기다리는 방식
스핀락(Spin Lock)은 임계 영역에 들어갈 수 없을 때 락이 해제될 때까지 루프를 돌며 다시 시도하는 커널 수준의 락 메커니즘이다. 운영체제 스케줄러의 대기 처리를 거치지 않고 Busy Waiting, 즉 Spinning으로 락 획득을 반복한다.
대기 중 문맥 교환이 없다는 점은 짧은 임계 영역에서 유리하다. 대신 락을 기다리는 동안에도 CPU를 계속 사용하므로, 대기가 길어지면 CPU 낭비와 시스템 Hang 위험으로 이어질 수 있다.
스핀락의 spin은 제자리에서 반복 확인한다는 뜻이다. 주로 커널 수준 동기화에 쓰이며, CPU를 점유한 채 기다리고 스케줄링 개입 없이 동작한다.
락 획득 전까지 반복하는 Busy Waiting
스핀락은 잠금 상태를 확인하고, 이미 잠겨 있으면 획득할 때까지 루프를 벗어나지 않는다.
// 스핀락 구현 예시
typedef struct {
int locked; // 0: unlocked, 1: locked
} spinlock_t;
void spin_lock(spinlock_t *lock) {
while (__sync_lock_test_and_set(&lock->locked, 1)) {
// 루프를 돌면서 대기 (Busy Waiting)
// CPU를 계속 사용
}
}
void spin_unlock(spinlock_t *lock) {
__sync_lock_release(&lock->locked);
}
락을 보유한 스레드가 임계 영역을 끝내고 Unlock할 때까지, 대기 스레드는 CPU를 사용하며 같은 락을 재시도한다.
대기 방식에 따라 CPU 사용 양상도 달라진다. 스핀락 대기는 100% CPU를 사용하지만 문맥 교환 없이 빠르게 반응한다. 슬립락 대기는 휴면 상태로 0% CPU를 사용하며, 그 대신 문맥 교환이 발생한다.
스케줄러에 대기 처리를 맡기지 않는 락
스핀락을 기다리는 스레드는 Running 상태를 유지한다. 반면 일반 락인 뮤텍스는 대기 스레드를 Blocked 상태로 옮기고, 스케줄러가 대기 큐를 관리한다.
| 항목 | 스핀락 | 일반 락 (뮤텍스) |
|---|---|---|
| 대기 방식 | Busy Waiting | Block & Sleep |
| 스케줄링 | 개입 안 함 | 대기 큐 관리 |
| 상태 전이 | Running 유지 | Running → Blocked |
| CPU 사용 | 100% | 0% (대기 시) |
이 특성 때문에 스핀락은 커널 스케줄러와 무관하게 동작하며, 스케줄링 오버헤드 없이 반응해야 하는 커널 내부와 인터럽트 핸들러에 제한적으로 사용된다. 락이 해제되는 즉시 획득을 시도할 수 있지만, 단일 코어에서는 대기 자체가 코어 낭비가 된다.
짧은 임계 영역에서 문맥 교환을 피하는 이점
스핀락은 대기 중에도 Running 상태에 머물므로 문맥 교환이 0회다. 뮤텍스는 Blocked 상태 전이와 Wakeup으로 문맥 교환이 2회 발생한다.
레지스터 저장과 복원이 필요 없고 캐시 무효화가 없으며, 락을 얻으면 즉시 실행을 계속할 수 있다. 따라서 대기 시간이 수십~수백 사이클 이내이고, Spinning 비용이 문맥 교환 비용보다 작은 경우에 효율적이다. 간단한 카운터 증가나 플래그 설정이 이에 해당한다.
성능을 비교하면 스핀락의 락 획득 시간은 Spinning 시간이고, 뮤텍스의 락 획득 시간은 Spinning 시간에 문맥 교환 비용인 수천 사이클이 더해진다.
락 보유자를 실행하지 못하게 만들 때의 위험
스핀락은 우선순위 역전과 단일 코어 환경에서 시스템 Hang을 일으킬 수 있다. 낮은 우선순위의 락 보유자가 CPU를 얻지 못하면 Unlock할 수 없고, 높은 우선순위 프로세스는 계속 Spinning 상태에 남는다.
긴 임계 영역도 스핀락과 맞지 않는다. 락을 기다리는 동안 유용한 작업을 할 수 없고, CPU 100% 사용은 전력 낭비로 이어진다. 다른 프로세스의 CPU 사용도 지연된다. 우선순위 상속과 멀티코어 환경은 이런 문제를 줄이는 방법이 될 수 있다.
세마포어와 선택 기준이 갈리는 지점
세마포어는 Block-Wakeup 방식으로 대기하며 CPU를 양보한다. 스핀락은 빠른 응답을 위해 CPU를 100% 점유하고, 세마포어는 문맥 교환을 감수하는 대신 긴 대기에서 CPU를 아낀다.
| 항목 | 스핀락 | 세마포어 (Block-Wakeup) |
|---|---|---|
| 대기 상태 | Busy Waiting (Running) | Sleep (Blocked) |
| CPU 사용 | 100% 점유 | 0% (양보) |
| 스케줄링 | 미개입 | 대기 큐 관리 |
| 응답 속도 | 빠름 (즉시) | 느림 (문맥 교환) |
| 적합한 경우 | 짧은 임계 영역 | 긴 임계 영역 |
짧은 CS에는 스핀락을, 긴 CS에는 세마포어를 적용하는 형태로 구분할 수 있다.
스핀락:
spin_lock(&lock);
count++; // 짧은 CS (수 사이클)
spin_unlock(&lock);
세마포어:
sem_wait(&sem);
process_data(); // 긴 CS (수천 사이클)
sem_post(&sem);
임계 영역이 수십 사이클이면 스핀락을 고려할 수 있고, 수천 사이클에 이르면 세마포어나 뮤텍스가 맞는다. 멀티코어에서는 락 보유자가 다른 코어에서 실행될 수 있지만, 단일 코어에서는 세마포어·뮤텍스 쪽이 적합하다.
커널과 인터럽트 핸들러에서의 사용
스핀락은 스케줄러, 메모리 관리, I/O 서브시스템 같은 커널 내부와 빠른 응답이 필요해 Sleep할 수 없는 인터럽트 핸들러에 적합하다. 멀티코어에서 락 보유자가 다른 코어에서 실행되고, 임계 영역이 수십 사이클 이내인 경우에도 사용할 수 있다. 실시간 시스템에서는 예측 가능한 최악 응답 시간이 필요한 상황에 적용된다.
반대로 I/O 대기나 복잡한 계산처럼 임계 영역이 긴 경우, 우선순위 역전 위험이 있는 단일 코어, 애플리케이션 수준 동기화가 필요한 사용자 공간, 모바일·임베디드처럼 전력 제약이 큰 환경에는 적합하지 않다.
Linux 커널에서는 간단한 플래그 설정이나 연결 리스트 조작처럼 짧은 작업을 보호할 수 있다.
// 간단한 플래그 설정
spin_lock(&my_lock);
flag = 1;
spin_unlock(&my_lock);
// 연결 리스트 조작
spin_lock(&list_lock);
list_add(&new_node, &head);
spin_unlock(&list_lock);
하드웨어 제어에서는 디바이스 레지스터 접근을 보호하는 데도 사용할 수 있다.
// 디바이스 레지스터 접근
spin_lock_irqsave(&hw_lock, flags);
writel(value, device_reg);
spin_unlock_irqrestore(&hw_lock, flags);