스레드 생성과 관리

Jaemyeong Lee·2024년 11월 13일

게임 서버1

목록 보기
110/220

std::thread (C++11)

기본 생성과 종료 대기

#include <iostream>
#include <thread>

void Worker(int id) {
    std::cout << "worker " << id << '\n';
}

int main() {
    std::thread t(Worker, 1);
    t.join();  // t가 끝날 때까지 현재 스레드 대기
}
  • std::thread 객체는 "실행 중인 OS 스레드 핸들"을 소유합니다.
  • 가장 중요한 규칙: 생성했으면 반드시 join 또는 detach로 정리해야 합니다.

join(), detach(), joinable() 핵심 규칙

  • join() : 스레드 종료를 기다리고 소유권을 정리
  • detach() : 백그라운드로 분리, 이후 join() 불가
  • joinable() : 아직 정리되지 않은 스레드인지 확인
std::thread t(Worker, 1);
if (t.joinable()) {
    t.join();
}
  • 매우 중요: std::thread 객체가 joinable 상태로 파괴되면 std::terminate()가 호출됩니다.
  • 즉 "그냥 두면 알아서 정리"가 아니라, 프로그램이 강제 종료될 수 있습니다.

인자 전달 (값/참조/이동)

#include <functional>
#include <thread>

void AddOne(int& x) { ++x; }

int main() {
    int n = 10;
    std::thread t(AddOne, std::ref(n));  // 참조 전달은 std::ref 필요
    t.join();                            // n == 11
}
  • 기본 전달은 값 복사입니다.
  • 참조 전달은 std::ref/std::cref를 써야 의도대로 동작합니다.
  • std::unique_ptr 같은 move-only 타입은 std::move로 넘깁니다.

여러 스레드 생성 시 안전 패턴

#include <thread>
#include <vector>

void Job(int id);

int main() {
    std::vector<std::thread> threads;
    threads.reserve(8);

    for (int i = 0; i < 8; ++i) {
        threads.emplace_back(Job, i);
    }

    for (auto& t : threads) {
        if (t.joinable()) {
            t.join();
        }
    }
}
  • emplace_back으로 바로 생성하면 불필요한 임시 객체를 줄일 수 있습니다.
  • "생성 루프 + join 루프"를 분리하는 패턴은 가장 기본적인 실무 패턴입니다.

하드웨어 동시성과 스레드 수 결정

unsigned int hw = std::thread::hardware_concurrency();
  • hardware_concurrency()힌트값이며 0을 반환할 수도 있습니다.
  • CPU 바운드 작업은 보통 코어 수 근처에서 시작해 측정으로 튜닝합니다.
  • I/O 바운드는 더 많은 스레드가 유리할 수 있지만, 과하면 컨텍스트 스위칭 비용이 커집니다.

실무 안전 수칙

detach()는 왜 위험한가

  • detach() 후에는 종료 시점/실패 여부를 추적하기 어렵습니다.
  • 로컬 변수나 this를 캡처한 상태에서 분리하면 수명 문제가 쉽게 발생합니다.
  • 게임 서버 로직에서는 보통 작업 큐 + 워커 풀로 대체합니다.

예외 안전과 RAII

  • 예외로 함수가 중간 탈출해도 join이 누락되지 않게 설계해야 합니다.
  • C++20에서는 std::jthread가 scope 종료 시 자동 join되어 안전성이 올라갑니다.
// C++20 예시
#include <chrono>
#include <thread>

void Loop(std::stop_token st) {
    while (!st.stop_requested()) {
        // 작업
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main() {
    std::jthread worker(Loop);  // scope 종료 시 자동 join
} // 자동 정리

스레드 생성 전략

전략특징서버 실무
요청마다 스레드 생성구현 단순, 생성/파괴 비용 큼대규모 서버에 비권장
고정 스레드 풀큐 기반 처리, 예측 가능가장 일반적
역할별 전담 스레드I/O/로직/DB 분리MMO에서 자주 사용

강의 시 유의사항

강조 포인트

  • std::thread의 본질은 "스레드 실행"보다 "수명 관리"입니다.
  • joinable()을 확인하지 않고 파괴하면 std::terminate()가 발생할 수 있음을 반드시 강조하세요.
  • hardware_concurrency()는 정답이 아니라 시작점입니다.

자주 하는 오해

오해바로잡기
join()을 안 해도 프로그램 종료 시 정리된다joinable 상태 파괴 시 std::terminate()
detach()는 성능 최적화 방법이다추적/동기화/수명 관리가 어려워질 수 있음
스레드를 많이 만들수록 항상 빠르다락 경합/컨텍스트 스위칭으로 느려질 수 있음

체크 질문 (스스로 답해보기)

  • std::thread 객체 소멸 시 왜 joinable() 상태가 위험한가?
  • 참조 인자 전달에서 std::ref가 필요한 이유는 무엇인가?
  • 요청마다 스레드를 만드는 방식보다 워커 풀이 유리한 이유는 무엇인가?

profile
李家네_공부방

0개의 댓글