5. SPSC 링버퍼의 메모리 순서(memory ordering) 버그

난이도 최상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 네트워크 수신 스레드 → 로직 스레드 사이를 잇는
// 단일 생산자/단일 소비자(SPSC) 락프리 링버퍼다.
//  - 생산자(net thread): 수신한 패킷을 Enqueue.
//  - 소비자(logic thread): Dequeue 해서 처리.
//  - "SPSC는 락이 필요 없다"는 전제로 인덱스 두 개로만 동기화한다.
//
// 운영 중 증상(드물고 비결정적):
//  - 아주 가끔 소비자가 "방금 막 넣은 패킷"의 내용이 비어 있거나(부분 기록)
//    엉뚱한(이전) 데이터를 읽는다.
//  - x86 벤치에선 재현이 거의 안 되는데, ARM(모바일 서버/일부 클라 호스트)
//    빌드에서 간헐적으로 깨진다.
//  - 가끔 버퍼가 가득/빈 판정이 어긋나 한 칸을 덮어쓰거나 빈 칸을 읽는다.
//
// 요구사항
// ----------------------------------------------------------------------------
//  - SPSC: 생산자 1, 소비자 1. 락 없이 동작.
//  - Enqueue 한 데이터는 Dequeue 측에서 "온전히, 순서대로" 보여야 한다.
//  - 멀티 아키텍처(x86 / ARM)에서 모두 정확해야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
//  1) 어떤 메모리 순서(memory ordering) 결함이 있는가?
//  2) (A)(B)(C)(D) 각 지점을 .NET 메모리 모델 관점에서 정확히 설명하라.
//  3) 올바른 acquire/release(Volatile) 동기화로 수정하라(x86에서 안 터지는 이유 포함).
// ============================================================================

using System.Threading;

namespace SpscRingBuffer
{
    public sealed class SpscRing<T>
    {
        private readonly int _mask;
        private readonly T[] _buf;

        // (A) 일반 int 필드. 동기화 한정자 없음.
        private int _head; // 생산자가 증가
        private int _tail; // 소비자가 증가

        public SpscRing(int capacityPow2)
        {
            // capacityPow2 는 2의 거듭제곱이라고 가정
            _mask = capacityPow2 - 1;
            _buf = new T[capacityPow2];
        }

        // 생산자(net thread) 전용
        public bool Enqueue(T item)
        {
            int head = _head;
            int tail = _tail;                 // (A) 그냥 읽기
            if (head - tail == _buf.Length)
                return false; // full

            _buf[head & _mask] = item;        // (B) 슬롯에 데이터 기록
            _head = head + 1;                 // (C) head 공개 (그냥 쓰기)
            return true;
        }

        // 소비자(logic thread) 전용
        public bool Dequeue(out T outItem)
        {
            int tail = _tail;
            int head = _head;                 // (D) 그냥 읽기
            if (head == tail)
            {
                outItem = default;
                return false; // empty
            }

            outItem = _buf[tail & _mask];     // 슬롯에서 데이터 읽기
            _tail = tail + 1;
            return true;
        }
    }

    // ---- 사용 예시 (개념용) ----
    // var ring = new SpscRing<Packet>(1024);
    // net thread:   while (running) { var p = Recv(); while(!ring.Enqueue(p)) {} }
    // logic thread: while (running) { if (ring.Dequeue(out var p)) Handle(p); }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.