락 (Lock) / 뮤텍스 (Mutex) — 스레드 간 상호 약속으로 임계 영역을 보호하는 메커니즘. 락을 쥔 스레드만 임계 영역에 진입하고, 잠근 스레드가 풀어준다. 공유 변수로 락 상태(잠김/풀림)와 소유자를 표현한다. 문제 제기와 다른 보호 방식은 공유 자원과 경쟁 상태 참고.
락 변수 구성
- 공유 변수
- Lock state — 잠김 / 풀림
- Lock owner — 누가 쥐고 있는가 (소유권은 필수는 아니지만 제대로 된 뮤텍스라면 잠근 스레드만 풀 수 있다)
순진한 구현과 그 취약점
플래그만 사용한 구현.
typedef struct _lock_t { int flag; } lock_t;
void init(lock_t *mutex) {
mutex->flag = 0; // 0 = available, 1 = held
}
void lock(lock_t *mutex) {
while (mutex->flag == 1) // TEST
; // spin-wait
mutex->flag = 1; // SET
}
void unlock(lock_t *mutex) {
mutex->flag = 0;
}문제: while 루프 탈출 직후·flag = 1 이전에 선점되면 두 스레드가 동시에 락을 얻는다. TEST와 SET이 원자적이지 않기 때문. 이 틈새를 막으려면 하드웨어 지원이 필요하다.
하드웨어 지원 원자 명령
Test-and-Set
“값 읽기 + 새 값 쓰기”를 한 명령에 원자적으로 수행.
int TestAndSet(int *old_ptr, int new) { // Atomic
int old = *old_ptr;
*old_ptr = new;
return old;
}Compare-and-Swap (CAS)
기대값과 일치할 때만 교체.
int compare_and_swap(int *reg, int oldval, int newval) { // Atomic
int old_reg_val = *reg;
if (old_reg_val == oldval)
*reg = newval;
return old_reg_val;
}안전한 구현 (Test-and-Set 기반)
void lock(lock_t *mutex) {
while (TestAndSet(&mutex->flag, 1) == 1)
; // spin-wait
}
void unlock(lock_t *mutex) {
mutex->flag = 0;
}TestAndSet이 원자적으로 “이전 값을 돌려주면서 1로 세팅”하므로 경쟁 창이 사라진다.
락 획득 실패 시 거동 — Spinlock vs Mutex
| 구분 | Spinlock | Mutex (Blocking Lock) |
|---|---|---|
| 대기 방식 | 락 변수를 계속 확인하며 루프 | 스레드를 대기 큐로 이동하고 블록 |
| CPU 사용 | 낭비 | 낭비 없음 |
| 진입 지연 | 추가 지연 없음 | waiting→ready 이동 + 새 스레드 선택 시간 |
| 적용 범위 | 멀티코어 간 공유 자원 (다른 코어가 풀어줌) | 코어 내부(intra-core) 공유 자원 |
| 해제 동작 | 플래그만 0으로 | unlock()이 블록된 스레드를 깨움 |
짧은 임계 영역 + 멀티코어 조합에는 spinlock이, 긴 임계 영역 + 단일 코어 상황에는 mutex가 맞다. 단일 코어에서 spinlock은 락을 쥔 스레드가 실행될 기회를 자신이 막기 때문에 의미가 없다.
락 입도 (Lock Granularity)
공유 자원이 많을 때 락을 어떻게 묶을 것인가 의 설계 선택.
| 방식 | 특성 |
|---|---|
| Fine-granular | 자원마다 개별 락 — 병렬성↑, 락 변수 많음, 코드 복잡도↑ |
| Coarse-granular | 여러 자원을 묶어 하나의 락으로 — 단순, 불필요한 블록 발생 |
결정적 트레이드오프: 병렬성 vs 단순성.
락이 유발하는 문제
락은 다시 두 가지 고질병을 낳는다.
두 문제를 완화·해결하는 프로토콜 모음은 리소스 공유 프로토콜 참고.