← 문제로

24. 두 엔티티 상호작용의 락 획득 순서 (ABBA 데드락)

난이도 최상
내 리뷰 · C#
해설 · C#

해설 — 두 엔티티 상호작용의 락 획득 순서 (ABBA 데드락)

난이도: 최상

답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계

요약

두 락을 인자 순서대로 잡는다. (A) a.Lock → (B) b.Lock. 스레드1 이 Link(x, y) 를, 스레드2 가 Link(y, x) 를 동시에 호출하면, 스레드1 은 x 를 잡고 y 를 기다리고, 스레드2 는 y 를 잡고 x 를 기다린다 → ABBA 교착(데드락). 두 락 사이의 DoSomeWork() 가 창을 넓혀 재현이 쉬워진다. 게다가 자기 자신(a == b)에 대해서도 같은 락을 두 번 잡는데, C# lock 은 재진입 가능이라 이 경우는 멈추지 않지만(주의: 다른 동기화 도구면 자기 교착) 의미상 검증이 필요하다. 정답 한 줄: 모든 코드가 락을 "전역적으로 동일한 순서"(예: Id 오름차순)로 획득하게 만들어 순환 대기를 원천 차단하라.

변별: problem8(ReaderWriterLockSlim 쓰기 기아/재진입)·problem9(락프리 스택 ABA)와 달리, 본 문제는 다중 락의 획득 순서로 인한 순환 대기(데드락) — 락 계층/전순서가 핵심이다.


문제점

(A)(B) 인자 순서 기반 락 획득 — 순환 대기/데드락 (동시성) ★간판

  • 분류 태그: lock-ordering deadlock / ABBA / circular wait.
  • 증상: 양방향 동시 호출 시 두 스레드가 서로가 쥔 락을 영원히 기다린다. 해당 워커 스레드가 묶이고, 그 락에 의존하는 다른 요청까지 연쇄로 멈춘다(전역 장애로 번질 수 있음).
  • 재현 조건: 같은 두 엔티티에 대해 반대 방향((x,y)(y,x))으로 동시 진입. 두 락 사이의 작업이 길수록 확률↑.
  • 근본 원인: 데드락 4조건 중 순환 대기(circular wait) 를 깨지 못함. 락 획득에 전역적 전순서(total order)가 없다.

(보강) 자기 자신/유효성

  • 분류 태그: self-interaction / validation.
  • 증상: a == b 면 같은 락을 두 번 — C# lock(Monitor)은 재진입 가능이라 통과하지만, 비재진입 동기화(SemaphoreSlim 등)였다면 자기 교착. 의미상 자기 링크는 보통 거부해야 한다.

수정안 (정확한 코드)

전역 락 순서를 강제한다. 두 락을 항상 Id 오름차순으로 획득(Id 동률이면 객체 식별자로 타이브레이크). 그러면 어떤 호출이든 같은 순서로 잡아 순환이 불가능하다.

public void Link(Entity a, Entity b)
{
    if (ReferenceEquals(a, b)) return;          // 자기 자신 거부(또는 단일 락 처리)

    // 전역 전순서: Id 오름차순. Id 동률 시 RuntimeHelpers 로 타이브레이크.
    Entity first = a, second = b;
    if (a.Id > b.Id ||
        (a.Id == b.Id &&
         System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(a)
         > System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(b)))
    {
        first = b; second = a;
    }

    lock (first.Lock)
    {
        DoSomeWork();
        lock (second.Lock)
        {
            a.Bond++;
            b.Bond++;
        }
    }
}

핵심: 인자 순서가 아니라 객체의 전역 순서로 락을 잡는다. 스레드1·스레드2 모두 Id 작은 쪽을 먼저 잡으므로 순환 대기가 성립하지 않는다.

대안: 한 쌍을 보호하는 단일 락(pair lock)으로 묶거나, 상호작용을 단일 스레드/큐로 직렬화하면 다중 락 자체가 사라진다.

더 나은 설계 (+ 트레이드오프)

  • 락 계층(lock hierarchy): 자원에 레벨을 부여하고 "낮은 레벨 → 높은 레벨" 순서만 허용. 디버그 빌드에서 순서 위반을 어서션으로 잡는다. 트레이드오프: 규율/도구 필요.
  • try-lock + 백오프: Monitor.TryEnter 로 둘째 락 획득 실패 시 첫째를 놓고 재시도(라이브락 방지를 위해 랜덤 백오프). 순서 강제가 어려운 동적 상황에 유효(트레이드오프: 복잡·기아).
  • 상호작용 직렬화: 두 엔티티가 속한 존/룸을 한 스레드가 소유하면 락 없이 순차 처리. 크로스-존 상호작용은 메시지로(트레이드오프: 지연).
  • 락 보유 중 외부 호출 금지: 두 락을 쥔 채 콜백/네트워크/DB 호출을 하면 데드락·지연이 급증. 임계영역을 짧게.

면접 포인트

  1. 데드락 4조건과 그중 실무에서 가장 잘 깨는 것 — 순환 대기 제거(전역 락 순서). 왜 "Id 같은 키로 정렬"이 정답인지 설명.
  2. C# lock(Monitor)은 재진입 가능, SemaphoreSlim 은 아님 — 자기 교착 차이. 두 락 사이의 작업을 줄여 창을 좁히는 것도 완화책(근본은 순서).
  3. try-lock+백오프 vs 전순서의 트레이드오프(라이브락/기아 위험), 그리고 "락 쥐고 외부 호출 금지" 원칙.