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#
내 답안 · 자동 저장

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