ONE QUESTION AT A TIME.

천천히 생각하고, 분명하게.

TODAY’S QUESTION401 / 1500
동시성난이도 하5분 답변 목표 · 보강본

두 스레드가 락 A와 B를 반대 순서로 잡다가 서로 멈췄습니다. 왜 스스로 풀리지 않으며 어떻게 예방하나요?

잠깐, 답변을 보기 전에
먼저 내 말로 설명해 볼까요?

답변과 점검 포인트 펼치기답변 접기
01

구두 답변

정의를 외우기보다, 원리와 조건을 연결해 설명해 보세요.

교착 상태는 여러 실행 흐름이 서로 가진 자원을 기다려서 아무도 진행할 수 없는 상태입니다. 락이 잠시 붐비는 것과는 다릅니다. 첫 스레드가 A를 잡고 B를 기다리는데, 다른 스레드는 B를 잡고 A를 기다리면 락이 풀릴 일이 없는 것이죠.

예를 들어 스레드 1은 A를 가진 채 B를 기다리고, 스레드 2는 B를 가진 채 A를 기다립니다. 기다리는 동안 먼저 잡은 락을 놓지 않으므로 둘 중 하나가 저절로 진행할 수 없습니다. 모든 경로가 A 다음 B를 잡게 하면 이 반대 방향의 대기 고리를 만들지 않게 할 수 있습니다.

이때 자원을 독점하고, 하나를 가진 채 다른 것을 기다리고, 남이 강제로 빼앗을 수 없고, 대기 관계가 고리를 이룹니다. 이것이 교착을 설명하는 네 가지 Coffman 조건입니다. 실제 코드를 고칠 때 저는 조건 이름을 나열하기보다 어떤 대기 관계를 끊을 수 있는지 보겠습니다.

두 계정의 잔액을 함께 바꾸는 경우라면 계정 ID가 작은 쪽부터 잠그는 식으로 순서를 통일하겠습니다. 모든 경로가 같은 순서로 잡으면 서로 반대 순서로 기다리는 고리를 만들지 않기 때문입니다. C++의 scoped_lock 같은 다중 락 획득 도구도 쓸 수 있지만, 호출 전에 이미 잡은 다른 락까지 해결해 주는 것은 아닙니다. 따라서 함수 하나뿐 아니라 그 함수를 부르는 경로도 봐야 합니다.

특히 락을 잡은 채 콜백이나 외부 요청의 완료를 기다리는 코드는 주의하겠습니다. 상대가 다시 같은 락을 필요로 하면 코드에서 바로 보이지 않는 대기 고리가 생깁니다. 가능하면 락 안에서는 필요한 상태만 읽고 외부 작업은 밖에서 하겠습니다. 대신 돌아와서 변경을 적용하기 전에는 버전이 바뀌었는지 다시 확인해야 합니다. 락을 풀었던 사이의 변경을 무시하면 교착은 줄여도 데이터가 잘못될 수 있습니다.

운영에서 멈춤이 의심된다면 시간 간격을 두고 스레드 덤프를 받아 같은 락의 소유자와 대기자가 유지되는지 확인하겠습니다. DB라면 교착 그래프로 관련 쿼리와 획득 순서를 보겠습니다. 타임아웃을 넣는 경우에도 이미 잡은 자원을 해제하고 부분 변경을 정리할 수 있어야 합니다. 기다림을 중단시키는 것과 안전하게 복구하는 것은 다르기 때문에, 재시도까지 포함해 실제로 다시 진행되는지 확인하겠습니다.

멈춤의 원인을 대기 관계로 표현합니다

스레드를 정점으로 두고 누가 가진 자원을 누가 기다리는지 연결하면 대기 그래프(wait-for graph)를 만들 수 있습니다. 단일 소유 자원에 대해 순환 대기가 생겼는지 보면 단순한 긴 작업과 교착을 구분하는 데 도움이 됩니다. 자원에 여러 인스턴스가 있거나 외부에서 공급될 수 있다면 사이클만으로 충분하지 않을 수 있으므로 가정을 명시하겠습니다. 소유자가 계속 진행하며 결국 락을 놓는다면 긴 대기일 수 있고, 서로가 다음 자원을 영원히 기다린다면 교착입니다.

자원을 독점하는 상호 배제, 잡은 채 기다리는 점유 대기, 강제로 빼앗지 못하는 비선점, 고리를 이루는 순환 대기를 Coffman의 필요 조건이라고 합니다. 이름을 외우는 것보다 지금 구현에서 어떤 조건을 끊을지 선택해야 합니다. 모든 잠금을 같은 순서로 획득하면 순환 대기를 막을 수 있고, 필요한 자원을 한 번에 확보하지 못하면 보유분을 반납하는 정책은 점유 대기를 줄일 수 있습니다. 하지만 자원 확보 비용과 재시도 경쟁이 늘므로 새로운 실패 양상도 확인해야 합니다.

풀기 쉬운 락만 순서를 맞추면 충분하지 않습니다

계정 ID 순서로 잠그는 규칙을 만들었다면 관리자 경로·실패 복구·콜백에서도 같은 순서를 지켜야 합니다. 동일 계정을 두 번 잠그는 요청이나 주소가 재사용되는 객체를 주소순으로 정렬하는 구현도 주의해야 합니다. 전역적으로 안정적인 순서가 무엇인지 정하고, 재귀 락을 사용해 문제를 숨기지 않겠습니다. 재귀 락은 같은 스레드가 같은 락을 다시 얻는 문제만 다룰 뿐 서로 다른 스레드 사이의 의존 사이클을 해결하지 않습니다.

락을 가진 채 사용자 콜백을 호출하면 그 콜백이 어떤 락을 얻는지 현재 함수만 보고 알기 어렵습니다. 외부 RPC 응답을 기다리는데 상대가 다시 현재 서비스의 잠긴 경로를 호출하는 경우에는 네트워크까지 대기 그래프가 확장됩니다. 로컬 덤프와 분산 추적을 연결해 기다림의 원인과 상관 ID를 확인하겠습니다. 프레임워크의 이벤트 처리나 로깅도 내부 락을 가질 수 있어, 임계 구역 안에서는 실행하는 코드의 범위를 작게 유지하는 편이 낫습니다.

대기를 끊은 뒤의 복구도 설계합니다

try-lock에 실패하면 모두 락을 놓고 다시 시도하는 두 스레드가 같은 박자로 계속 양보하면 움직이지만 진전이 없는 라이브락(livelock)이 생길 수 있습니다. 한 요청만 계속 락을 얻지 못하는 것은 기아(starvation)입니다. 교착과 원인이 다르므로 무작위 백오프·공정한 대기·재시도 상한을 검토하겠습니다. 타임아웃이 났다고 이미 보유한 락을 자동으로 모두 놓거나 부분 변경을 복구해 주는 것은 아니므로 예외와 취소 경로의 정리를 확인해야 합니다.

DB의 교착 탐지는 한 트랜잭션을 희생자로 중단해 다른 쪽을 진행시키는 방식일 수 있습니다. 애플리케이션은 중단된 트랜잭션 전체를 안전하게 재시도할지 사용자에게 실패를 돌려줄지 정해야 합니다. 외부 호출은 DB 롤백과 별개라는 점도 남습니다. 검증은 두 스레드가 첫 락을 각각 얻은 시점에 맞춰 다음 락을 요청하도록 통제하고, 수정 뒤에는 그 순서에서도 완료·실패·재시도가 제한 시간 안에 정리되는지 확인하겠습니다.

02

득점 포인트

이해가 드러나는 지점을 확인해 보세요.

  • 경합과 교착을 대기 관계로 구분한다. 잠깐의 지연과 스스로 풀리지 않는 순환 대기를 구별한다.
  • 잠금 순서를 사례에 적용한다. 계정 ID 순서 같은 일관된 기준이 순환 대기를 어떻게 끊는지 설명한다.
  • 락 밖으로 작업을 옮기는 비용을 설명한다. 외부 호출을 분리한 뒤 상태 버전을 재검증해야 함을 짚는다.
  • 진단과 복구를 구체화한다. 덤프·교착 그래프로 경로를 찾고 자원 해제와 부분 변경 정리를 확인한다.
03

감점 포인트

놓치기 쉬운 조건과 오해를 짚어 보세요.

  • “락을 오래 기다리면 교착이다”라고 말한다. 소유자와 대기자의 의존 관계 및 실제 진행 여부를 봐야 한다.
  • “RAII를 쓰면 교착이 없다”고 말한다. 범위 종료 시 해제는 순환 대기를 막는 보장이 아니다.
  • “외부 호출 전에 락을 풀기만 하면 된다”고 말한다. 다시 잠근 시점에는 앞서 읽은 상태가 유효하지 않을 수 있다.
  • “타임아웃 뒤 바로 재시도한다”고 말한다. 자원과 부분 상태를 정리하지 않으면 문제가 남거나 경쟁이 반복된다.
04

더 파고들 거리

하나 더 질문하며 이해의 범위를 넓혀 보세요.

득점·감점 포인트는 학습용 자기 점검 기준이며, 특정 회사의 실제 채점표가 아닙니다.

원리부터 공부하고 싶다면

답변을 보충하는 개념·예제·슈도코드를 읽어 보세요.

← 전체 질문 보기