스핀락이란?
정의
- 스핀락은 락이 풀릴 때까지 스레드가 잠들지 않고 반복 시도(busy-wait) 하는 락입니다.
- 커널 스케줄링 전환 비용 없이 빠르게 재시도할 수 있어, 매우 짧은 임계 영역에서 유리할 수 있습니다.
언제 문제가 되는가
- 락 보유 시간이 길거나 경합이 높으면 CPU를 계속 태워 오히려 성능이 떨어집니다.
- 특히 스레드 수가 코어 수를 크게 초과하면 spin은 대기 비용이 더 커집니다.
잘못된 구현과 실패 원인
잘못된 코드
bool flag = false;
void lock() {
while (flag) {}
flag = true;
}
void unlock() {
flag = false;
}
왜 깨지는가
while(flag) 확인과 flag=true 설정이 분리되어 있어 원자적이지 않습니다.
- 두 스레드가 거의 동시에 통과하면 둘 다 락을 획득한 것처럼 동작할 수 있습니다.
flag가 비원자 공유 변수라 데이터 레이스 자체로 UB(정의되지 않은 동작) 입니다.
올바른 구현 1 - atomic_flag
최소 구현
#include <atomic>
#include <thread>
class SpinLock {
public:
void lock() {
while (flag_.test_and_set(std::memory_order_acquire)) {
std::this_thread::yield();
}
}
void unlock() {
flag_.clear(std::memory_order_release);
}
private:
std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
};
메모리 오더 핵심
acquire : lock 이후의 읽기/쓰기가 lock 이전으로 재배치되지 않게 보장
release : unlock 이전의 읽기/쓰기가 unlock 이후로 밀리지 않게 보장
- 즉, 임계 영역 내 변경이 다른 스레드에게 올바르게 관측됩니다.
올바른 구현 2 - CAS(atomic<bool>)
CAS 기반 구현
std::atomic<bool> flag{false};
void lock() {
bool expected = false;
while (!flag.compare_exchange_weak(expected, true,
std::memory_order_acquire,
std::memory_order_relaxed)) {
expected = false;
std::this_thread::yield();
}
}
void unlock() {
flag.store(false, std::memory_order_release);
}
weak vs strong
- 반복 루프에서는
weak를 많이 사용합니다(실패 가능성 포함해도 재시도 구조와 잘 맞음).
- 단발 비교에서 불필요한 재시도를 줄이고 싶다면
strong을 고려합니다.
실무 성능 팁 (Backoff)
단순 spin의 한계
- 경합이 높은 구간에서
while만 돌면 캐시 라인 경쟁과 전력 소모가 커집니다.
점진 백오프 예시
void lock_with_backoff() {
int spins = 0;
while (flag_.test_and_set(std::memory_order_acquire)) {
if (spins < 10) {
++spins;
} else {
std::this_thread::yield();
}
}
}
- 경합이 짧을 때는 빠르게 획득하고, 길어지면 CPU 양보로 손실을 줄입니다.
- 플랫폼별로
pause 명령(x86 _mm_pause)을 추가하면 스핀 효율이 더 좋아질 수 있습니다.
언제 쓰고 언제 피할까
| 상황 | 스핀락 적합성 | 이유 |
|---|
| 임계 영역이 매우 짧음(수십~수백 ns) | 높음 | 커널 전환 없이 빠른 재시도 |
| 코어 여유가 충분함 | 높음 | 바쁜 대기가 덜 치명적 |
| 긴 작업/블로킹 I/O 포함 | 낮음 | 대기 중 CPU 낭비 심함 |
| 스레드 과구독(threads >> cores) | 낮음 | 스케줄링 지연과 경합 악화 |
| 단일 코어 환경 | 낮음 | holder가 실행되지 못해 대기 악순환 |
MMO 관점
- 매우 짧은 락(예: 작은 상태 플래그, 짧은 큐 헤더 보호)에서는 유리할 수 있습니다.
- 게임 로직/DB/I/O처럼 오래 걸릴 수 있는 구간에는
mutex 또는 큐 기반 분리가 더 안전합니다.
강의 시 유의사항
강조 포인트
- 스핀락의 본질은 "빠른 락"이 아니라 "짧은 임계 영역에서만 유효한 선택지"입니다.
- 구현 시 반드시
acquire/release 의미를 함께 설명하세요.
- naive 구현이 왜 UB인지(비원자 공유 변수)까지 짚어야 학습 완성도가 올라갑니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| 스핀락은 무조건 mutex보다 빠르다 | 경합/락 보유 시간이 길면 오히려 더 느림 |
| CAS만 쓰면 안전하다 | memory order와 사용 맥락까지 맞아야 안전 |
yield()는 필요 없다 | 고경합에서 CPU 과열/낭비를 줄이는 데 유용 |
체크 질문 (스스로 답해보기)
- 스핀락
unlock()에 memory_order_release가 필요한 이유는?
- 코어 수보다 스레드가 많은 환경에서 스핀락이 왜 불리한가?
- 당신의 코드에서 스핀락을 써도 되는 임계 영역 길이는 어느 정도인가?