교착 상태는 서로 기다리는 고리를 어떻게 끊을까
두 작업이 각자 필요한 자원을 하나씩 가진 채 상대의 자원을 기다리면 아무도 앞으로 나아가지 못할 수 있다. 교착 상태는 작업들이 서로가 일으켜야 할 사건을 기다려 진행이 멈춘 상황이다. CPU가 바쁘지 않아도 프로그램이 멈출 수 있고, 오류 메시지 없이 요청이 계속 대기하는 형태로 드러나기도 한다.
여러 잠금을 일관된 순서로 획득하면 이런 순환 대기를 막을 수 있다.
잠금 두 개로 만드는 최소 예제
스레드 A는 잠금 X를 얻고 Y를 기다린다. 스레드 B는 Y를 얻고 X를 기다린다. X는 A가, Y는 B가 놓아야 하지만 두 작업 모두 두 번째 잠금을 얻기 전에는 첫 잠금을 놓지 않는다. 어느 쪽도 진행할 수 없게 된다. 잠금 각각은 올바르게 상호 배제를 제공하고 있어도 조합 순서가 전체 진행을 막는 것이다.
자원 모델에서 교착 상태를 설명할 때 상호 배제, 점유하면서 대기, 비선점, 순환 대기의 네 조건을 함께 다룬다. 자원을 공유해 동시에 사용할 수 없고, 이미 가진 자원을 놓지 않고 추가 자원을 기다리며, 강제로 빼앗을 수 없고, 기다림이 고리를 만드는 조건이다. OSTEP의 동시성 버그 장은 이 조건과 방지 접근을 설명한다.
획득 순서를 정하면 고리를 막는다
모든 작업이 X를 먼저, Y를 나중에 얻도록 통일하면 B가 Y를 먼저 가진 채 X를 기다리는 고리가 생기지 않는다. 자원에 일관된 전체 순서를 부여하고 항상 그 순서대로 획득하는 방법이다. 같은 종류의 객체가 많다면 안정적인 식별자 순서 등을 사용할 수 있다. 호출하는 함수 내부에서도 이 규칙을 유지하는지 확인해야 한다.
다른 방법은 필요한 자원을 한 번에 확보하거나, 추가 획득에 실패하면 가진 것을 놓고 다시 시도하는 것이다. 그러나 무작정 놓고 재시도하면 두 작업이 계속 서로를 피하면서 진전하지 못하는 라이브락이나 특정 작업의 기아가 생길 수 있다. 재시도 자체는 교착 방지의 설명을 끝내지 않으며 진행 보장과 공정성을 함께 검토해야 한다.
예방·회피·탐지는 서로 다르다
예방은 구조적으로 조건 하나가 성립하지 않게 만드는 접근이다. 회피는 자원 요청을 허용했을 때 앞으로도 작업들이 완료할 수 있는 안전한 상태인지 고려한다. 은행원 알고리즘은 최대 자원 요구를 아는 모델에서 이를 설명한다. 탐지는 일단 발생 가능성을 두고 대기 관계를 조사해 문제가 있으면 작업 중단이나 복구로 해결하는 접근이다.
자원 종류마다 인스턴스가 하나라면 대기 고리가 교착을 설명하기 쉽다. 한 종류에 여러 인스턴스가 있는 모델에서는 고리가 있다는 사실만으로 언제나 교착이라고 단정할 수 없다. 누군가 다른 인스턴스를 사용하고 끝내 기다림을 풀 수 있기 때문이다. 그래프를 해석할 때 자원 수와 요청 조건을 함께 명시해야 한다.
교착 상태는 잠금뿐 아니라 스레드 풀이 자신의 큐에 넣은 작업을 기다리는 상황, 서로의 응답을 기다리는 서비스, 메시지 순서에서도 생긴다. 진단할 때는 누가 무엇을 보유하고 무엇을 기다리는지 연결해 본다. 단순히 오래 기다린다는 증상과 달리, 교착은 필요한 진행이 대기 고리 안에서 막혀 있다는 구조로 설명해야 한다.