← 문제로

33. 매칭 레이팅 갱신 vs 매칭 진행: 찢긴 스냅샷과 갱신 유실 (C#)

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

해설 — 매칭 레이팅 갱신 vs 매칭 진행: 찢긴 스냅샷과 갱신 유실 (C#)

난이도: 하

요약

매칭 워커가 Mmr 을 동기화 없이 읽고, 결과 워커가 Mmr·Tier 를 락 없이 갱신한다. 그래서 (1) 배치 시점에 MmrTier 가 서로 어긋난 찢긴 스냅샷을 볼 수 있고, (2) 여러 매치 결과가 동시에 정산되면 Mmr += delta 의 read-modify-write 가 겹쳐 조정이 유실된다.

문제점

  • 분류 태그: 갱신 유실(lost update, 비원자 RMW) · 찢긴 읽기(다중 필드 비원자 갱신) · 가시성(안전 공개 부재).
  • 증상: 랭킹/티어가 실제 승패 합과 안 맞음(예: 두 판 이겼는데 한 판만 반영), 브래킷 배치가 방금 바뀐 Mmr 과 어긋난 Tier 로 이뤄져 실력대가 튐.
  • 재현 조건:
    • 갱신 유실: 결과 워커 R1(delta=+20), R2(delta=+15)가 동시 ApplyResult. 둘 다 p.Mmr(=1000) 을 읽음 → R1 이 1020, R2 가 1015 로 씀 → 하나가 사라짐(최종 1020, 기대 1035).
    • 찢긴 스냅샷: 결과 워커가 p.Mmr 은 새 값으로 쓴 직후, 매칭 워커가 그 새 Mmr 을 읽지만 Tier 는 아직 옛 값(또는 반대) → 불일치.
  • 근본 원인:
    • (B) p.Mmr = p.Mmr + delta 는 읽고-더하고-쓰기가 원자가 아니다. 동시 실행 시 서로의 쓰기를 덮어써 lost update. 또 MmrTier두 번의 별도 쓰기로 갱신해 그 사이 관측되면 짝이 안 맞는다.
    • (A) 매칭 워커가 락/배리어 없이 필드를 읽어, 갱신과의 순서·가시성이 보장되지 않는다(오래된/어긋난 값). C# 에서 int 단일 읽기·쓰기 자체는 찢기지 않지만, 여러 필드의 불변식RMW 원자성은 언어가 지켜 주지 않는다.

수정안

플레이어별 락으로 Mmr·Tier한 원자 구간에서 함께 갱신·조회한다.

class Player
{
    public int PlayerId;
    public int Mmr;
    public int Tier;
    public readonly object Gate = new();
}

public int PlaceInBracket(Player p)
{
    lock (p.Gate) { return p.Mmr / BracketWidth; }   // 배치도 같은 락으로 일관 조회
}

public void ApplyResult(Player p, int delta)
{
    lock (p.Gate)
    {
        p.Mmr += delta;                 // RMW 를 원자 구간으로
        p.Tier = ComputeTier(p.Mmr);    // Mmr·Tier 를 함께 갱신(짝 보장)
    }
}

레이팅만 단순 누적이고 파생 필드가 없다면 Interlocked.Add(ref p.Mmr, delta) 로 락 없이 처리할 수 있다. 다만 Tier 같은 파생값을 함께 일관되게 두려면 CAS 루프로 (Mmr,Tier) 를 불변 스냅샷 객체로 원자 교체하는 방법이 더 깔끔하다:

// 불변 Rating { Mmr, Tier } 을 Volatile 참조로 두고, Interlocked.CompareExchange 로 통째 교체.

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

  • 플레이어 소유 스레드로 직렬화: 한 플레이어의 상태 변경(정산)과 조회(배치)를 같은 워커/샤드로 몰면 락이 사라진다. 대신 크로스-스레드 요청 라우팅이 필요.
  • 불변 스냅샷 원자 교체: Rating 을 불변 객체로 만들어 Interlocked.CompareExchange 로 교체하면 읽는 쪽은 락 없이 항상 일관된 짝을 본다. 갱신 경합 시 CAS 재시도 비용.
  • 레이팅 갱신 직렬 큐: 결과 정산을 플레이어별 큐로 순차 처리하면 유실이 원천 차단되고 감사가 쉬워진다. 지연·구현 복잡도 증가.

면접 포인트

  1. "int 읽기/쓰기가 원자적" 이라는 사실이 RMW 원자성이나 다중 필드 불변식을 보장하지 않는 이유(lost update, 찢긴 스냅샷은 별개 문제).
  2. 파생값(Tier)을 원본(Mmr)과 같은 임계 구간에서 갱신해야 관측자가 짝이 맞는 값을 보는 이유.
  3. Interlocked/불변 스냅샷 교체 vs 락의 선택, 그리고 소유 스레드 직렬화로 경합 자체를 없애는 접근.