운영체제 · 동시성과 락 · 이론
동시성 — 순서를 정하지 않으면 결과가 정해지지 않는다
한 줄 요약
여러 실행 흐름이 같은 데이터를 건드리는 순간 결과가 실행 순서에 의존하게 되고, 그 순서를 강제하는 장치가 락이며, 락을 잘못 쓰면 아무도 진행하지 못하는 상태에 빠진다.
왜 이게 필요했나
counter = counter + 1 이라는 한 줄은 기계어로 최소 세 단계다. 읽고, 더하고, 쓴다. 두 스레드가 이 코드를 동시에 실행하면 둘 다 같은 값을 읽고 같은 값을 쓰는 일이 생겨 증가 한 번이 사라진다. 이것이 경쟁 상태(race condition) 다.
이 버그의 고약한 점은 재현성이다. 대부분의 실행에서는 아무 일도 일어나지 않다가, 부하가 올라가거나 코어 수가 늘어나면 나타난다. 그래서 테스트를 통과하고 운영에서 터진다.
어떻게 동작하나
공유 자원을 건드리는 코드 구간을 임계 구역이라 하고, 올바른 해법은 세 조건을 만족해야 한다.
1. 상호 배제 — 한 번에 하나만 임계 구역에 들어간다.
2. 진행 — 아무도 안에 없으면 들어가려는 쪽 중 하나는 반드시 들어간다.
3. 한정 대기 — 들어가려는 쪽이 무한정 밀리지 않는다.
소프트웨어만으로도 만들 수 있지만(피터슨 알고리즘) 현대 CPU 는 원자적 명령어를 제공한다. 비교하고 값이 같으면 바꾸는 CAS(compare-and-swap)가 대표적이고, 거의 모든 락과 무잠금 자료구조가 이 위에 서 있다.
동기화 도구는 성격이 다르다.
- 뮤텍스 — 소유자가 있다. 잠근 스레드만 풀 수 있다.
- 세마포어 — 카운터다. 소유자 개념이 없어 A 가 획득하고 B 가 반납해도 된다. 자원 개수를 제한할 때 맞다.
- 조건 변수 — "어떤 조건이 참이 될 때까지 기다린다"를 표현한다. 반드시 뮤텍스와 짝을 이루고, 깨어난 뒤 조건을 다시 검사해야 한다(가짜 깨어남이 있다).
대기 방식도 갈린다. 스핀락은 풀릴 때까지 CPU 를 돌며 기다린다. 임계 구역이 아주 짧고 코어가 여유로울 때만 이득이며, 그렇지 않으면 CPU 를 태울 뿐이다. 블로킹 락은 스레드를 재우고 다른 일을 하게 하지만 컨텍스트 스위치 비용이 든다.
데드락은 네 조건이 동시에 성립할 때만 생긴다. 상호 배제, 점유하며 대기, 비선점, 순환 대기. 넷 다 필요하다는 말은 하나만 깨면 막을 수 있다는 뜻이기도 하다. 실무에서 가장 자주 쓰이는 방법이 순환 대기를 깨는 것, 즉 모든 코드가 락을 같은 순서로 획득하게 하는 규칙이다. 데이터베이스에서 데드락이 잦다면 트랜잭션이 행을 건드리는 순서가 제각각인지부터 본다.
우선순위 역전도 알아 둘 만하다. 낮은 우선순위 스레드가 락을 쥔 채 중간 우선순위 스레드에 밀려 실행되지 못하면, 그 락을 기다리는 높은 우선순위 스레드까지 함께 막힌다. 화성 탐사선 패스파인더의 유명한 재부팅 사고가 이 문제였고, 해법은 락을 쥔 스레드의 우선순위를 일시적으로 올려 주는 우선순위 상속이다.
현장에서 만나는 모습
애플리케이션에서 "재고를 확인하고 없으면 차감"처럼 읽고 판단한 뒤 쓰는 패턴은 락 없이는 언제나 깨진다. 그런데 여기서 흔한 오해가 하나 있다. 데이터베이스의 격리 수준을 올리면 해결된다는 믿음이다. 두 트랜잭션이 같은 집합을 읽고 서로 다른 행을 갱신하면 쓰기 충돌이 없으므로 스냅샷 격리는 이를 잡지 못한다. 이 현상을 쓰기 편향(write skew)이라 하고, 직렬화 가능 수준이나 명시적 잠금, 또는 제약 조건으로만 막을 수 있다.
이어지는 퀴즈에서 확인할 것
뮤텍스와 세마포어를 언제 나눠 쓰는지, 데드락 네 조건 중 무엇을 깨는 것이 현실적인지 판단할 수 있는지 확인한다.