7. ThreadLocal 버퍼 누수와 레지스트리 동시성
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 패킷 직렬화 경로다. 직렬화할 때마다 임시 버퍼가 필요한데,
// 매번 할당/해제하면 비싸므로 "스레드별 재사용 버퍼"를 두기로 했다.
// - 각 스레드가 자기 전용 직렬화 버퍼(SerializeBuffer)를 ThreadLocal 로 보유.
// - 동시에, 운영툴이 "현재 살아있는 버퍼들의 총 메모리"를 보고 싶어 해서,
// 버퍼가 생성될 때 전역 레지스트리에 자기 자신을 등록하도록 했다.
//
// 운영 중 증상:
// - 단기 작업용 스레드를 많이 생성/종료하는 워크로드(예: 매치메이킹 잡,
// 임시 IO 스레드 풀 확장/축소)에서 시간이 갈수록 메모리가 단조 증가한다.
// - 레지스트리에 등록된 버퍼 개수가 "현재 스레드 수"보다 훨씬 많아진다.
// - GetTotalBytes() 가 실제보다 점점 과대하게 나온다.
// - 가끔 레지스트리 집계 중 예외(컬렉션 변경/인덱스 오류)가 난다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 각 스레드는 자기 전용 직렬화 버퍼를 재사용한다(할당 비용 절감).
// - 스레드가 끝나면 그 버퍼의 메모리는 회수되고 레지스트리에서도 빠져야 한다.
// - GetTotalBytes() 는 현재 실제로 살아있는 버퍼 총량을 반영해야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 스레드를 반복 생성/종료하면 메모리/레지스트리가 어떻게 되는가? 왜?
// 2) (A)(B)(C) 각 지점의 결함을 설명하라.
// 3) 스레드 종료 시 정리되도록, 레지스트리 동시성까지 포함해 수정하라.
// ============================================================================
using System;
using System.Collections.Generic;
using System.Threading;
namespace SerializeBuffers
{
// 살아있는 버퍼들을 추적하는 전역 레지스트리(싱글턴)
public sealed class BufferRegistry
{
public static readonly BufferRegistry Instance = new BufferRegistry();
// (C) 락 없이 일반 List 에 추가
private readonly List<SerializeBuffer> _buffers = new();
// (B) 등록만 있고, 해제(Deregister)는 어디에도 없다
public void Register(SerializeBuffer b)
{
_buffers.Add(b);
}
public long GetTotalBytes()
{
long total = 0;
foreach (var b in _buffers)
total += b.Capacity;
return total;
}
}
public sealed class SerializeBuffer
{
private readonly byte[] _data;
public SerializeBuffer()
{
_data = new byte[64 * 1024]; // 64KB 작업 버퍼
BufferRegistry.Instance.Register(this); // 생성 시 등록
}
public int Capacity => _data.Length;
public byte[] Data => _data;
}
public static class Serializer
{
// (A) 스레드별 재사용 버퍼. ThreadLocal 인데 Dispose 도, 해제도 없다.
private static readonly ThreadLocal<SerializeBuffer> _tls =
new ThreadLocal<SerializeBuffer>(() => new SerializeBuffer());
// 직렬화 핫패스(개념용)
public static void SerializePacket(int payloadByte)
{
var b = _tls.Value;
b.Data[0] = (byte)payloadByte;
// ... 실제 직렬화 생략 ...
}
}
// 데모용 구동 코드
public static class Demo
{
public static void Run()
{
// 단기 스레드를 반복 생성/종료하는 워크로드 흉내
for (int round = 0; round < 5; round++)
{
var ts = new Thread[8];
for (int i = 0; i < 8; i++)
{
ts[i] = new Thread(() =>
{
for (int k = 0; k < 100; k++)
Serializer.SerializePacket(k);
});
ts[i].Start();
}
foreach (var t in ts) t.Join();
Console.WriteLine($"round {round}: registry total = {BufferRegistry.Instance.GetTotalBytes()} bytes");
}
// 기대: 매 라운드 스레드가 끝나면 버퍼도 회수되어 total 이 0 근처로 돌아와야 함
Console.WriteLine("done");
}
}
} 결함 코드 · C++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 패킷 직렬화 경로다. 직렬화할 때마다 임시 버퍼가 필요한데,
// 매번 할당/해제하면 비싸므로 "스레드별 재사용 버퍼"를 두기로 했다.
// - 각 스레드가 자기 전용 직렬화 버퍼(SerializeBuffer)를 thread_local 로 보유.
// - 동시에, 운영툴이 "현재 살아있는 버퍼들의 총 메모리"를 보고 싶어 해서,
// 버퍼가 생성될 때 전역 레지스트리에 자기 자신을 등록하도록 했다.
//
// 운영 중 증상:
// - 단기 작업용 스레드를 많이 생성/종료하는 워크로드(예: 매치메이킹 잡,
// 임시 IO 스레드 풀 확장/축소)에서 시간이 갈수록 메모리가 단조 증가한다.
// - 레지스트리에 등록된 버퍼 개수가 "현재 스레드 수"보다 훨씬 많아진다.
// - GetTotalBytes() 가 실제보다 점점 과대하게 나온다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 각 스레드는 자기 전용 직렬화 버퍼를 재사용한다(할당 비용 절감).
// - 스레드가 끝나면 그 버퍼의 메모리는 회수되고 레지스트리에서도 빠져야 한다.
// - GetTotalBytes() 는 현재 실제로 살아있는 버퍼 총량을 반영해야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 스레드를 반복 생성/종료하면 메모리/레지스트리가 어떻게 되는가? 왜?
// 2) (A)(B)(C) 각 지점의 결함을 설명하라.
// 3) 스레드 종료 시 정리되도록, 레지스트리 동시성까지 포함해 수정하라.
// ============================================================================
#include <cstdio>
#include <cstdint>
#include <mutex>
#include <thread>
#include <vector>
class SerializeBuffer;
// 살아있는 버퍼들을 추적하는 전역 레지스트리
class BufferRegistry
{
public:
static BufferRegistry& Get()
{
static BufferRegistry inst;
return inst;
}
// (B) 등록만 있고, 해제(deregister)는 어디에도 없다
void Register(SerializeBuffer* b)
{
// (C) 락 없이 전역 벡터에 추가
buffers_.push_back(b);
}
std::size_t GetTotalBytes() const;
private:
std::vector<SerializeBuffer*> buffers_;
};
class SerializeBuffer
{
public:
SerializeBuffer()
: data_(64 * 1024) // 64KB 작업 버퍼
{
BufferRegistry::Get().Register(this); // 생성 시 등록
}
std::size_t Capacity() const { return data_.size(); }
std::uint8_t* Data() { return data_.data(); }
private:
std::vector<std::uint8_t> data_;
};
// (A) 스레드별 재사용 버퍼. raw new 로 만들고 영원히 안 지움.
static SerializeBuffer& TlsBuffer()
{
thread_local SerializeBuffer* buf = new SerializeBuffer();
return *buf;
}
// 직렬화 핫패스(개념용)
static void SerializePacket(int payloadByte)
{
SerializeBuffer& b = TlsBuffer();
b.Data()[0] = static_cast<std::uint8_t>(payloadByte);
// ... 실제 직렬화 생략 ...
}
std::size_t BufferRegistry::GetTotalBytes() const
{
std::size_t total = 0;
for (auto* b : buffers_)
total += b->Capacity();
return total;
}
int main()
{
// 단기 스레드를 반복 생성/종료하는 워크로드 흉내
for (int round = 0; round < 5; ++round)
{
std::vector<std::thread> ts;
for (int i = 0; i < 8; ++i)
ts.emplace_back([]() {
for (int k = 0; k < 100; ++k)
SerializePacket(k);
});
for (auto& t : ts) t.join();
std::printf("round %d: registry total = %zu bytes\n",
round, BufferRegistry::Get().GetTotalBytes());
}
// 기대: 매 라운드 스레드가 끝나면 버퍼도 회수되어 total 이 0 근처로 돌아와야 함
std::printf("done\n");
return 0;
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.