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++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 네트워크 수신 스레드 → 로직 스레드 사이를 잇는
// 단일 생산자/단일 소비자(SPSC) 락프리 링버퍼다.
// - 생산자(net thread): 수신한 패킷 포인터를 Enqueue.
// - 소비자(logic thread): Dequeue 해서 처리.
// - "SPSC는 락이 필요 없다"는 전제로 atomic 인덱스 두 개로만 동기화한다.
//
// 운영 중 증상(드물고 비결정적):
// - 아주 가끔 소비자가 "방금 막 넣은 패킷"의 내용이 비어 있거나(부분 기록)
// 엉뚱한(이전) 데이터를 읽는다.
// - x86 벤치에선 재현이 거의 안 되는데, ARM(모바일 서버/일부 클라 호스트)
// 빌드에서 간헐적으로 깨진다.
// - 가끔 버퍼가 가득/빈 판정이 어긋나 한 칸을 덮어쓰거나 빈 칸을 읽는다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - SPSC: 생산자 1, 소비자 1. 락 없이 동작.
// - Enqueue 한 데이터는 Dequeue 측에서 "온전히, 순서대로" 보여야 한다.
// - 멀티 아키텍처(x86 / ARM)에서 모두 정확해야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 어떤 메모리 순서(memory ordering) 결함이 있는가?
// 2) (A)(B)(C)(D) 각 지점을 memory_order 관점에서 정확히 설명하라.
// 3) 올바른 acquire/release 동기화로 수정하라(x86에서 안 터지는 이유 포함).
// ============================================================================
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <vector>
template <typename T>
class SpscRing
{
public:
explicit SpscRing(std::size_t capacityPow2)
: mask_(capacityPow2 - 1), buf_(capacityPow2)
{
// capacityPow2 는 2의 거듭제곱이라고 가정
}
// 생산자(net thread) 전용
bool Enqueue(const T& item)
{
const std::size_t head = head_.load(std::memory_order_relaxed);
const std::size_t tail = tail_.load(std::memory_order_relaxed); // (A)
if (head - tail == buf_.size())
return false; // full
buf_[head & mask_] = item; // (B) 슬롯에 데이터 기록
head_.store(head + 1, std::memory_order_relaxed); // (C) head 공개
return true;
}
// 소비자(logic thread) 전용
bool Dequeue(T& out)
{
const std::size_t tail = tail_.load(std::memory_order_relaxed);
const std::size_t head = head_.load(std::memory_order_relaxed); // (D)
if (head == tail)
return false; // empty
out = buf_[tail & mask_]; // 슬롯에서 데이터 읽기
tail_.store(tail + 1, std::memory_order_relaxed);
return true;
}
private:
const std::size_t mask_;
std::vector<T> buf_;
std::atomic<std::size_t> head_{0}; // 생산자가 증가
std::atomic<std::size_t> tail_{0}; // 소비자가 증가
};
// ---- 사용 예시 (개념용) ----
// SpscRing<Packet*> ring(1024);
// net thread: while (running) { Packet* p = Recv(); while(!ring.Enqueue(p)) {} }
// logic thread: while (running) { Packet* p; if (ring.Dequeue(p)) Handle(p); } 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.