← 문제로

38. 틱 이중 버퍼 스왑의 동시성 결함 (가시성·찢긴 읽기, C#)

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

해설 — 틱 이중 버퍼 스왑의 동시성 결함 (가시성·찢긴 읽기, C#)

난이도: 최상

요약

_readIndexvolatile도 아니고 어떤 동기화도 없어, 워커 스레드가 (B)의 인덱스 갱신과 그 앞의 버퍼 쓰기를 재배열된 순서로 볼 수 있다. GC 덕에 UAF는 없지만, 워커가 시뮬레이션이 채우는 도중의 버퍼나 찢긴 스냅샷을 관측할 수 있다. C++판과 달리 크래시 대신 "잘못된 상태 브로드캐스트"로 나타난다.

문제점

  • [메모리 모델/가시성] 비volatile _readIndex. .NET 메모리 모델에서 일반 필드 쓰기/읽기는 스레드 간 즉시 가시성이나 순서를 보장하지 않는다. 워커가 _readIndex의 갱신을 늦게 보거나(옛 버퍼 계속 읽기), 인덱스 갱신을 버퍼 내용 갱신보다 먼저 볼 수 있다(부분적으로 쓰인 버퍼 읽기).
  • [찢긴 읽기 / 2버퍼 부족] 읽는 중 덮어쓰기. 워커가 (C)에서 참조를 얻어 Entities를 순회하는 도중, 후속 틱의 CommitTick이 같은 버퍼 객체를 다시 쓰기 대상으로 삼으면 List가 동시 수정된다 → InvalidOperationException("Collection was modified") 또는 찢긴 스냅샷. 워커의 읽기가 한 틱을 넘기면(느린 브로드캐스트) 반드시 발생 가능.
  • [참조 반환의 함정] CommitTick(next)가 외부 객체를 그대로 슬롯에 대입. 호출자가 next를 계속 들고 수정하면 게시된 스냅샷이 사후 변형된다(불변성 부재).

근본 원인

게시(인덱스 교체)에 메모리 배리어가 없고, 버퍼 수명이 소비자의 읽기 기간과 분리되지 않았다. 이중 버퍼는 "소비자가 한 틱 내에 읽기를 끝낸다"는 가정에 의존하는데 다수 워커·가변 지연에서 깨진다.

수정안

불변 스냅샷을 만들고, 게시를 Volatile.Write/Interlocked.Exchange로 원자화한다. 소비자는 참조를 스냅으로 받아 GC가 수명을 관리하게 한다(버퍼 재사용 제거).

public sealed class WorldPublisher
{
    private WorldSnapshot _current = new();

    // 시뮬레이션: 새 스냅샷을 만들어 원자적으로 게시.
    public void CommitTick(WorldSnapshot next)
    {
        // next 는 이후 수정하지 않는(불변) 새 인스턴스여야 한다.
        Volatile.Write(ref _current, next);          // release 의미의 게시
    }

    // 워커: 게시된 스냅샷 참조를 안전하게 획득.
    public WorldSnapshot Current() => Volatile.Read(ref _current);  // acquire
}
  • 참조 타입 필드 대입은 원자적이므로 찢김이 없고, Volatile.Write/Read가 순서(스냅샷 내용 ↔ 참조 게시)를 보장한다. 매 틱 새 인스턴스를 게시하므로, 워커가 붙잡은 옛 스냅샷은 GC가 살려둔다 → 덮어쓰기·동시 수정 없음.
  • 스냅샷을 진짜 불변으로(예: 게시 후 Entities를 건드리지 않기, 또는 IReadOnlyList로 노출) 만들면 사후 변형도 막는다.

더 나은 설계

  • 매 틱 할당이 부담이면 스냅샷 풀 + 세대 카운터로 재사용하되, "소비자가 읽는 스냅샷은 재사용 금지"를 참조 카운팅/에폭으로 강제한다.
  • 델타(변경분)만 게시해 스냅샷 크기와 할당을 줄인다.
  • 브로드캐스트가 오래 걸리면 워커가 스냅샷을 잡은 즉시 필요한 필드만 복사해 보유 시간을 줄인다.

면접 포인트

  • .NET에서 일반 필드 vs volatile/Volatile.Read/Write의 가시성·순서 차이.
  • 이중 버퍼가 깨지는 조건(읽기 기간 > 한 틱)과 불변 스냅샷+GC로 해결하는 원리.
  • C++판(UAF/크래시)과 C#판(예외/잘못된 상태)의 실패 양상 차이 — GC 유무가 만드는 차이.