6. Double-Checked Locking의 메모리 재배열 버그
난이도 하 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 전역 설정 캐시(GameConfig) 싱글턴이다.
// - 서버 부팅 후 첫 요청이 올 때 디스크/DB에서 설정을 로드해 캐시한다(지연 초기화).
// - 이후 모든 로직 스레드가 Instance 로 같은 설정 객체를 공유해 읽는다.
// - "락은 최초 1회 초기화에만 걸고, 그 뒤엔 락 없이 빠르게 읽자"는 의도로
// 이중 검사 잠금(double-checked locking, DCL) 패턴을 직접 구현했다.
//
// 운영 중 증상(드물고 비결정적):
// - 아주 가끔 초기화 직후 시점에, 일부 스레드가 설정의 필드(MaxPlayers 등)를
// 0 또는 기본값으로 읽는다. 잠시 후 다시 읽으면 정상값이 나온다.
// - 디버그 빌드/단일 코어에선 재현이 안 되고, 릴리스 + 다중 코어에서 간헐 발생.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 설정은 정확히 한 번만 로드되어야 한다(중복 로드 금지).
// - 초기화 이후 읽기는 락 없이 빠르게.
// - 어떤 스레드도 "부분 초기화된" 설정 객체를 보면 안 된다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 왜 가끔 부분 초기화된 객체가 보이는가? 왜 릴리스/다중코어에서만?
// 2) (A)(B)(C) 각 지점의 결함을 메모리 모델 관점에서 설명하라.
// 3) 안전한 지연 초기화로 수정하라(여러 방법과 트레이드오프).
// ============================================================================
using System;
using System.Threading;
namespace ConfigCache
{
public sealed class GameConfig
{
public int MaxPlayers;
public int TickRate;
public string MapName = "";
// 무거운 로드(디스크/DB 파싱 흉내)
public static GameConfig Load()
{
var c = new GameConfig();
// 여러 필드를 순차적으로 채운다 (시간이 걸린다)
Thread.SpinWait(1000);
c.MaxPlayers = 100;
c.TickRate = 60;
c.MapName = "arena_01";
return c;
}
}
public sealed class ConfigCache
{
// (A) 캐시된 인스턴스. 동기화 한정자 없음.
private static GameConfig _instance;
private static readonly object _lock = new object();
public static GameConfig Instance
{
get
{
// (B) 1차 검사: 락 없이 빠른 경로
if (_instance == null)
{
lock (_lock)
{
// 2차 검사
if (_instance == null)
{
// (C) 로드한 객체를 바로 대입
_instance = GameConfig.Load();
}
}
}
return _instance;
}
}
}
// 데모용 구동 코드
public static class Demo
{
public static void Run()
{
const int readers = 16;
var threads = new Thread[readers];
for (int t = 0; t < readers; t++)
{
threads[t] = new Thread(() =>
{
var cfg = ConfigCache.Instance;
// 부분 초기화면 0이 보일 수 있다
if (cfg.MaxPlayers == 0 || cfg.TickRate == 0)
Console.WriteLine("[BUG] partially initialized config observed");
});
threads[t].Start();
}
foreach (var th in threads) th.Join();
Console.WriteLine("done");
}
}
} 결함 코드 · C++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 전역 설정 캐시(GameConfig) 싱글턴이다.
// - 서버 부팅 후 첫 요청이 올 때 디스크/DB에서 설정을 로드해 캐시한다(지연 초기화).
// - 이후 모든 로직 스레드가 Instance 로 같은 설정 객체를 공유해 읽는다.
// - "락은 최초 1회 초기화에만 걸고, 그 뒤엔 락 없이 빠르게 읽자"는 의도로
// 이중 검사 잠금(double-checked locking, DCL) 패턴을 직접 구현했다.
//
// 운영 중 증상(드물고 비결정적):
// - 아주 가끔 초기화 직후 시점에, 일부 스레드가 설정의 필드(MaxPlayers 등)를
// 0 또는 기본값으로 읽는다. 잠시 후 다시 읽으면 정상값이 나온다.
// - x86 빌드/단일 코어에선 재현이 안 되고, ARM(모바일 서버) + 다중 코어에서 간헐 발생.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 설정은 정확히 한 번만 로드되어야 한다(중복 로드 금지).
// - 초기화 이후 읽기는 락 없이 빠르게.
// - 어떤 스레드도 "부분 초기화된" 설정 객체를 보면 안 된다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 왜 가끔 부분 초기화된 객체가 보이는가? 왜 ARM/다중코어에서만?
// 2) (A)(B)(C) 각 지점의 결함을 메모리 모델 관점에서 설명하라.
// 3) 안전한 지연 초기화로 수정하라(여러 방법과 트레이드오프).
// ============================================================================
#include <cstdio>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
struct GameConfig
{
int MaxPlayers = 0;
int TickRate = 0;
std::string MapName;
// 무거운 로드(디스크/DB 파싱 흉내)
static GameConfig* Load()
{
GameConfig* c = new GameConfig();
// 여러 필드를 순차적으로 채운다 (시간이 걸린다)
volatile int spin = 0;
for (int i = 0; i < 10000; ++i) spin += i;
c->MaxPlayers = 100;
c->TickRate = 60;
c->MapName = "arena_01";
return c;
}
};
class ConfigCache
{
public:
static GameConfig* Instance()
{
// (B) 1차 검사: 락 없이 빠른 경로
if (instance_ == nullptr)
{
std::lock_guard<std::mutex> lk(mutex_);
// 2차 검사
if (instance_ == nullptr)
{
// (C) 로드한 객체를 바로 대입
instance_ = GameConfig::Load();
}
}
return instance_;
}
private:
// (A) 캐시된 인스턴스. 동기화 한정자 없음.
static GameConfig* instance_;
static std::mutex mutex_;
};
GameConfig* ConfigCache::instance_ = nullptr;
std::mutex ConfigCache::mutex_;
// 데모용 구동 코드
int main()
{
constexpr int readers = 16;
std::vector<std::thread> threads;
threads.reserve(readers);
for (int t = 0; t < readers; ++t)
{
threads.emplace_back([]() {
GameConfig* cfg = ConfigCache::Instance();
// 부분 초기화면 0이 보일 수 있다
if (cfg->MaxPlayers == 0 || cfg->TickRate == 0)
std::printf("[BUG] partially initialized config observed\n");
});
}
for (auto& th : threads) th.join();
std::printf("done\n");
return 0;
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.