33. 매칭 레이팅 갱신 vs 매칭 진행: 찢긴 스냅샷과 갱신 유실 (C#)
난이도 하해설 — 매칭 레이팅 갱신 vs 매칭 진행: 찢긴 스냅샷과 갱신 유실 (C#)
난이도: 하
요약
매칭 워커가 Mmr 을 동기화 없이 읽고, 결과 워커가 Mmr·Tier 를 락 없이 갱신한다. 그래서 (1) 배치 시점에 Mmr 과 Tier 가 서로 어긋난 찢긴 스냅샷을 볼 수 있고, (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는 아직 옛 값(또는 반대) → 불일치.
- 갱신 유실: 결과 워커 R1(delta=+20), R2(delta=+15)가 동시
- 근본 원인:
- (B)
p.Mmr = p.Mmr + delta는 읽고-더하고-쓰기가 원자가 아니다. 동시 실행 시 서로의 쓰기를 덮어써 lost update. 또Mmr과Tier를 두 번의 별도 쓰기로 갱신해 그 사이 관측되면 짝이 안 맞는다. - (A) 매칭 워커가 락/배리어 없이 필드를 읽어, 갱신과의 순서·가시성이 보장되지 않는다(오래된/어긋난 값). C# 에서
int단일 읽기·쓰기 자체는 찢기지 않지만, 여러 필드의 불변식과 RMW 원자성은 언어가 지켜 주지 않는다.
- (B)
수정안
플레이어별 락으로 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 재시도 비용. - 레이팅 갱신 직렬 큐: 결과 정산을 플레이어별 큐로 순차 처리하면 유실이 원천 차단되고 감사가 쉬워진다. 지연·구현 복잡도 증가.
면접 포인트
- "
int읽기/쓰기가 원자적" 이라는 사실이 RMW 원자성이나 다중 필드 불변식을 보장하지 않는 이유(lost update, 찢긴 스냅샷은 별개 문제). - 파생값(
Tier)을 원본(Mmr)과 같은 임계 구간에서 갱신해야 관측자가 짝이 맞는 값을 보는 이유. Interlocked/불변 스냅샷 교체 vs 락의 선택, 그리고 소유 스레드 직렬화로 경합 자체를 없애는 접근.
해설 — 매칭 레이팅 갱신 vs 매칭 진행: 데이터 레이스와 갱신 유실 (C++)
난이도: 하
요약
매칭 워커가 mmr 을 동기화 없이 읽고, 결과 워커가 mmr·tier 를 락 없이 갱신한다. C++ 에서는 이처럼 한 스레드가 쓰고 다른 스레드가 읽는 평범한 int 접근 자체가 데이터 레이스(UB) 이며, mmr += delta 의 read-modify-write 는 동시 실행 시 갱신이 유실되고, mmr·tier 를 별도로 써서 찢긴 스냅샷이 관측된다.
문제점
- 분류 태그: 데이터 레이스(동기화 없는 동시 read/write = UB) · 갱신 유실(비원자 RMW) · 찢긴 읽기(다중 필드 비원자 갱신).
- 증상: 티어/레이팅이 승패 합과 불일치(정산 유실), 브래킷 배치가 어긋난
mmr/tier조합으로 이뤄짐. 최적화 빌드에선 컴파일러가 경합 값을 레지스터에 캐싱/재정렬해 더 기이한 결과. - 재현 조건:
- 유실:
applyResult(p,+20)와applyResult(p,+15)동시 실행 → 둘 다p.mmr(=1000)읽고 각각 1020/1015 로 씀 → 하나 소실. - 레이스: 결과 워커가
p.mmr갱신 중인데 매칭 워커가p.mmr을 읽음 → UB(찢김/오래된 값), 그리고mmr새값 +tier옛값 조합 관측.
- 유실:
- 근본 원인:
- (B)
p.mmr = p.mmr + delta는 원자가 아니다. 동시 실행 시 lost update.mmr과tier를 두 번에 나눠 써서 그 사이 관측 시 불일치. - (A) 매칭 워커가 아무 동기화 없이
p.mmr을 읽는다. C++ 메모리 모델상 동기화 없는 동시 접근(하나 이상이 쓰기) 은 정의되지 않은 동작이다. "보통 int 는 원자적" 은 표준 보장이 아니다.
- (B)
수정안
플레이어별 mutex 로 mmr·tier 를 한 임계 구간에서 함께 갱신·조회한다.
#include <mutex>
struct Player {
int playerId;
int mmr;
int tier;
std::mutex m;
};
int Matchmaker::placeInBracket(Player& p) {
std::lock_guard<std::mutex> lk(p.m);
return p.mmr / kBracketWidth; // 배치도 같은 락으로 일관 조회
}
void Matchmaker::applyResult(Player& p, int delta) {
std::lock_guard<std::mutex> lk(p.m);
p.mmr += delta; // RMW 를 원자 구간으로
p.tier = computeTier(p.mmr); // mmr·tier 를 함께 갱신
}
파생 필드가 없다면 std::atomic<int> mmr 에 fetch_add(RMW 원자) + load(레이스 제거)로 락 없이 처리할 수 있다. 하지만 tier 처럼 함께 일관돼야 하는 값이 있으면, (mmr,tier) 를 하나의 스냅샷 구조체로 만들어 std::atomic<Snapshot>(또는 atomic<shared_ptr<Snapshot>>) 로 통째 교체하는 편이 낫다.
더 나은 설계 (트레이드오프)
- 플레이어 소유 스레드 직렬화: 정산·배치를 같은 샤드/워커로 몰면 락 제거. 대신 요청 라우팅 필요.
- 불변 스냅샷 원자 교체: 작은 POD 스냅샷을
std::atomic으로 교체하면 읽는 쪽이 락 없이 일관된 짝을 본다. 스냅샷이 크면atomic<shared_ptr>로. CAS 재시도 비용. - 원자 정수 + 배리어:
mmr만 필요하면atomic<int>fetch_add로 충분. 파생 계산은 읽는 쪽에서 그때그때 유도.
면접 포인트
- C++ 에서 동기화 없는 동시 read/write 는 그 자체로 UB — "int 는 원자적일 것" 이라는 가정이 왜 틀리는가(
std::atomic이 필요). - RMW 원자성(
fetch_add)과 다중 필드 불변식(스냅샷 원자 교체)은 서로 다른 문제라는 점. - 락 vs
atomicvsatomic<shared_ptr>스냅샷의 선택 기준, 그리고 소유 스레드 직렬화로 경합을 제거하는 접근.