43. thread_local 저장 기간의 초기화·소멸 시점과 순서

난이도 중 해설 보기 →

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

결함 코드 · C++
// [주제] thread_local 저장 기간의 초기화·소멸 시점과 순서
//
// 게임서버는 워커 스레드별 로그 버퍼, 스레드 로컬 오브젝트 풀, 스레드 로컬 난수 엔진처럼
// thread_local 을 광범위하게 쓴다. 아래 프로그램은 그 수명(lifetime)을 추적한다.
//
// [과제]
//  1) 이 프로그램의 표준 출력이 어떤 순서로 찍힐지 예측하라.
//     특히 다음을 각각 근거와 함께 답하라.
//     - g_tlNamespace 는 언제, 어느 스레드에서 몇 번 생성되는가?
//     - Worker1 은 LogBuffer 와 ObjectPool 을 어떤 순서로 만들고 어떤 순서로 파괴하는가?
//     - Worker2 는 Worker1 과 같은 개수의 thread_local 을 만드는가?
//     - main 스레드의 thread_local 과 g_staticGlobal 중 어느 쪽이 먼저 파괴되는가?
//  2) 함수 로컬 thread_local 의 동적 초기화는 언제 일어나며,
//     매 호출마다 컴파일러가 무엇을 검사하는가? 그 비용은 얼마인가?
//  3) 스레드 종료 시 thread_local 소멸자가 호출되는 순서를 결정하는 규칙은 무엇인가?
//  4) DumpOnExit 의 소멸자는 안전한가? 왜 그렇게 판단했는지 설명하라.
//  5) RunPooledTask 가 같은 스레드에서 두 번 호출될 때 출력이 왜 그렇게 나오는지 설명하라.
//  6) LeakPointer 가 반환한 포인터를 다른 스레드에서 쓰면 무슨 일이 생기는가?

#include <iostream>
#include <random>
#include <string>
#include <thread>
#include <vector>

struct Tracer {
    std::string name;
    explicit Tracer(std::string n) : name(std::move(n)) {
        std::cout << "    [ctor] " << name << '\n';
    }
    ~Tracer() { std::cout << "    [dtor] " << name << '\n'; }
    void Touch() const {}
};

// ── 정적 저장 기간 ────────────────────────────────────────────────────────
Tracer g_staticGlobal("static  : g_staticGlobal");

// ── 네임스페이스 스코프 thread_local ──────────────────────────────────────
thread_local Tracer g_tlNamespace("tl:ns   : g_tlNamespace");

// ── 함수 로컬 thread_local ────────────────────────────────────────────────
Tracer& LogBuffer() {
    static thread_local Tracer buf("tl:func : LogBuffer");
    return buf;
}

Tracer& ObjectPool() {
    thread_local Tracer pool("tl:func : ObjectPool");
    return pool;
}

// 소멸자에서 다른 thread_local 을 만진다
struct DumpOnExit {
    ~DumpOnExit() {
        std::cout << "    [dtor] tl:func : DumpOnExit -> touching LogBuffer\n";
        LogBuffer().Touch();
    }
};

DumpOnExit& Dumper() {
    thread_local DumpOnExit d;
    return d;
}

// ── 스레드 로컬 상태의 재사용 ─────────────────────────────────────────────
int& RequestCounter() {
    thread_local int counter = 0;
    return counter;
}

std::mt19937& Rng() {
    thread_local std::mt19937 engine(12345);
    return engine;
}

void RunPooledTask(const char* taskName) {
    ++RequestCounter();
    std::cout << "  " << taskName << ": counter=" << RequestCounter()
              << " roll=" << (Rng()() % 100) << '\n';
}

// ── thread_local 주소를 밖으로 넘긴다 ─────────────────────────────────────
Tracer* LeakPointer() { return &LogBuffer(); }

// ── 워커 ──────────────────────────────────────────────────────────────────
void Worker1() {
    std::cout << "  Worker1: begin\n";
    ObjectPool().Touch();
    LogBuffer().Touch();
    Dumper();
    g_tlNamespace.Touch();
    std::cout << "  Worker1: end\n";
}

void Worker2() {
    std::cout << "  Worker2: begin\n";
    LogBuffer().Touch();
    RunPooledTask("Worker2/taskA");
    RunPooledTask("Worker2/taskB");
    std::cout << "  Worker2: end\n";
}

int main() {
    std::cout << "main: begin\n";

    std::cout << "main: spawn Worker1\n";
    std::thread(Worker1).join();

    std::cout << "main: spawn Worker2\n";
    std::thread(Worker2).join();

    std::cout << "main: touch LogBuffer on main thread\n";
    Tracer* p = LeakPointer();
    std::cout << "main: leaked ptr name = " << p->name << '\n';

    std::cout << "main: end\n";
    return 0;
}
내 리뷰 · C++
내 답안 · 자동 저장

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