← 문제로

40. 디스패치 스레드 블로킹과 교차 세션 상호 데드락 (C#)

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

해설 — 디스패치 스레드 블로킹과 교차 세션 상호 데드락 (C#)

난이도: 상

요약

세션 핸들러(디스패치 스레드에서 실행)가 상대 세션에 보낸 비동기 질의를 .Result동기 블로킹 (A) 한다. 상대의 응답 로직 역시 같은(또는 한정된) 디스패치 스레드에서 실행되므로, (1) 블로킹 동안 그 스레드가 담당하는 다른 모든 세션 처리가 정지하고, (2) 두 세션이 서로에게 질의하면 서로의 응답을 기다리며 영구 교착한다. 최악은 스레드풀 기아까지 겹치는 sync-over-async 데드락이다.

문제점

  • [분류: 이벤트 루프/리액터 스레드 블로킹 · sync-over-async 데드락 · 상호 대기]
    • 증상: 특정 거래·결투 요청 이후 관련 세션들이 응답 없이 멈추고, 심하면 해당 IO 스레드가 담당하던 무관한 플레이어들까지 렉/무응답. 재현은 간헐적.
    • 재현:
      1. 상호 데드락: A→B 거래 제안 처리 중 AskConfirmTrade(B).Result 로 블록. 동시에 B→A 제안이 들어와 B 스레드가 AskConfirmTrade(A).Result 로 블록. A 는 B 응답을, B 는 A 응답을 기다림 → 둘 다 진행 불가.
      2. 디스패치 기아: AskConfirmTrade 의 continuation 이 실행될 디스패치 스레드를 호출자가 .Result 로 점유 중이라 continuation 이 스케줄될 스레드가 없어(SynchronizationContext/제한 풀), 자기 자신을 기다림.
    • 근본 원인: 동기 대기로 처리 스레드를 점유한 것. 액터 모델의 전제(각 세션은 자기 스레드에서 순차·비블로킹 처리)를 깨고, 처리 흐름 안에서 다른 액터의 완료를 블로킹으로 기다렸다.

수정안

교차 세션 통신을 비동기 메시지 패싱으로 바꾼다. 핸들러를 async 로 만들고 await 로 처리 스레드를 반납한 뒤, 응답이 오면 continuation 에서 이어서 처리한다. 동기 대기(.Result/.Wait())를 제거하는 것이 핵심.

public async Task HandleTradeProposal(SessionActor from, long targetId, TradeOffer offer)
{
    var target = _sessions.Get(targetId);
    if (target == null) { from.Send(new TradeRejected("상대 없음")); return; }

    // await 로 디스패치 스레드를 반납 → 상대 응답 대기 중에도 다른 세션 처리 진행
    bool accepted = await target.AskConfirmTrade(from.PlayerId, offer)
                                .ConfigureAwait(false);

    if (accepted) { _sessions.ExecuteTrade(from, target, offer); from.Send(new TradeAccepted()); }
    else            from.Send(new TradeRejected("상대가 거절"));
}
  • 상호 데드락도 사라진다: await 는 스레드를 점유하지 않으므로 A·B 가 서로를 기다려도 각자의 continuation 이 자유롭게 스케줄된다.
  • 거래처럼 두 자원을 원자적으로 바꿔야 하면, 응답 수신 후 상관관계 ID로 원래 거래 컨텍스트를 찾아 ExecuteTrade 를 한 액터(또는 정렬된 락 순서)에서 확정한다.

더 나은 설계

  • 교차 엔티티 트랜잭션(거래)은 **제3의 조정자(거래 세션/트랜잭션 액터)**를 두어 두 플레이어에게 각각 비동기로 확인 요청을 보내고, 양쪽 확정을 모아 원자 커밋. 세션 액터끼리 직접 동기 호출하지 않는다.
  • 절대 규칙: 디스패치/IO 스레드에서 블로킹 금지(락 대기, .Result, 동기 DB/RPC).
  • 트레이드오프: 비동기·상관관계 ID·조정자 도입으로 코드 복잡도가 오른다. 그러나 스레드 점유가 사라져 확장성과 장애 격리가 크게 좋아진다.

면접 포인트

  • .Result/.Wait() 가 리액터/제한 스레드 모델에서 데드락을 부르는가 (continuation 이 실행될 스레드를 호출자가 점유).
  • 액터 간 상호작용은 동기 호출이 아니라 메시지 패싱이어야 하는 이유.
  • 두 엔티티 원자 연산을 비동기로 안전하게 확정하는 법(조정자/상관관계 ID/락 순서).