43. JIT 활성 수명 축소와 객체 조기 수집 (GC.KeepAlive / SafeHandle)

난이도 최상 해설 보기 →

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

결함 코드 · C#
// [주제] JIT 의 활성 수명(liveness) 축소와 객체 조기 수집 — GC.KeepAlive / SafeHandle
//
// 게임서버에서 네이티브 압축 라이브러리를 얇게 감싼 래퍼다. 핸들을 정수로
// 들고 있다가 파이널라이저에서 닫는다. 아래 프로그램의 출력을 예측하고
// 내부 동작으로 설명하라.
//
// [과제]
//  1) Run1 에서 무슨 일이 일어나는가? "그 인스턴스의 메서드가 아직 실행 중인데
//     인스턴스가 수집될 수 있는가"에 답하고, 관리 객체의 수명이 무엇으로
//     결정되는지 설명하라.
//  2) Run2 는 왜 Run1 과 다른가? GC.KeepAlive 의 실제 구현은 무엇이며,
//     아무 일도 하지 않는 메서드 호출이 어떻게 수명을 늘리는가?
//  3) Run3 이 이 문제를 구조적으로 푸는 방식을 설명하라. SafeHandle 의 참조
//     카운트와 P/Invoke 마샬링이 각각 무엇을 보장하는가?
//  4) Run4 의 출력과, 부활(resurrection)한 객체의 이후 운명을 설명하라.
//     파이널라이저는 어느 스레드에서 언제 도는가?
//  5) Debug 빌드나 디버거를 붙이면 Run1 의 결과가 달라질 수 있다. 이유는?
//     워크스테이션 GC 와 서버 GC 에서는 어떤 차이가 있는가?
//  6) 이 버그가 게임서버에서 어떤 증상으로 나타나는지(소켓 래퍼, 풀링된
//     네이티브 버퍼, 압축 컨텍스트) 서술하고 올바른 설계를 제시하라.

using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;

public static class NativeLib   // 네이티브 라이브러리 흉내
{
    private static readonly ConcurrentDictionary<int, bool> Alive = new();
    private static int _next;

    public static int Open() { int h = Interlocked.Increment(ref _next); Alive[h] = true; return h; }

    public static void Close(int h)
    {
        Alive[h] = false;
        Console.WriteLine($"    [native] close(handle={h})");
    }

    public static int Compress(int h, int size)
    {
        if (!Alive.TryGetValue(h, out bool ok) || !ok)
            throw new InvalidOperationException($"handle {h} is already closed");
        return size / 2;
    }

    public static void Collect() { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); }
}

public sealed class NaiveCompressor              // (1) 정수 핸들 + 파이널라이저
{
    private readonly int _handle;

    public NaiveCompressor() => _handle = NativeLib.Open();
    ~NaiveCompressor() => NativeLib.Close(_handle);

    public int Compress(byte[] payload)
    {
        int h = _handle;                          // this 의 마지막 사용
        NativeLib.Collect();                      // 긴 작업 도중 GC 가 일어난다고 가정
        return NativeLib.Compress(h, payload.Length);
    }

    public int CompressKeepAlive(byte[] payload)
    {
        int h = _handle;
        NativeLib.Collect();
        int result = NativeLib.Compress(h, payload.Length);
        GC.KeepAlive(this);
        return result;
    }
}

public sealed class CompressHandle : SafeHandle  // (2) SafeHandle 기반
{
    public CompressHandle() : base(IntPtr.Zero, ownsHandle: true)
        => SetHandle(new IntPtr(NativeLib.Open()));

    public override bool IsInvalid => handle == IntPtr.Zero;

    protected override bool ReleaseHandle() { NativeLib.Close(handle.ToInt32()); return true; }

    public int Compress(int size)
    {
        bool added = false;
        try
        {
            DangerousAddRef(ref added);
            NativeLib.Collect();
            return NativeLib.Compress(handle.ToInt32(), size);
        }
        finally { if (added) DangerousRelease(); }
    }
}

public sealed class PooledBuffer                 // (3) 부활
{
    public static PooledBuffer Revived;
    public readonly int Id;

    public PooledBuffer(int id) => Id = id;

    ~PooledBuffer()
    {
        Console.WriteLine($"    [finalizer] thread={Environment.CurrentManagedThreadId} id={Id}");
        Revived = this;                           // 자기 자신을 정적 필드에 대입
    }
}

public static class Program
{
    private static readonly byte[] Payload = new byte[1024];

    private static void RunNaive()
        => Console.WriteLine($"  result={new NaiveCompressor().Compress(Payload)}");

    private static void RunKeepAlive()
        => Console.WriteLine($"  result={new NaiveCompressor().CompressKeepAlive(Payload)}");

    private static void RunSafeHandle()
    {
        var h = new CompressHandle();
        Console.WriteLine($"  result={h.Compress(Payload.Length)}");
        GC.KeepAlive(h);
    }

    private static void RunResurrect()
    {
        new PooledBuffer(7);
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine($"  Revived is null? {PooledBuffer.Revived is null}");

        PooledBuffer.Revived = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("  second collect done");
    }

    public static void Main()
    {
        var runs = new (string Name, Action Run)[]
        {
            ("Run1 naive", RunNaive), ("Run2 keepalive", RunKeepAlive),
            ("Run3 safehandle", RunSafeHandle), ("Run4 resurrect", RunResurrect),
        };

        foreach (var (name, run) in runs)
        {
            Console.WriteLine(name);
            try { run(); }
            catch (Exception ex) { Console.WriteLine($"  EXCEPTION: {ex.GetType().Name}: {ex.Message}"); }
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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