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();
}
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));
t.join();
}
- 기본 전달은 값 복사입니다.
- 참조 전달은
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되어 안전성이 올라갑니다.
#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);
}
스레드 생성 전략
| 전략 | 특징 | 서버 실무 |
|---|
| 요청마다 스레드 생성 | 구현 단순, 생성/파괴 비용 큼 | 대규모 서버에 비권장 |
| 고정 스레드 풀 | 큐 기반 처리, 예측 가능 | 가장 일반적 |
| 역할별 전담 스레드 | I/O/로직/DB 분리 | MMO에서 자주 사용 |
강의 시 유의사항
강조 포인트
std::thread의 본질은 "스레드 실행"보다 "수명 관리"입니다.
joinable()을 확인하지 않고 파괴하면 std::terminate()가 발생할 수 있음을 반드시 강조하세요.
hardware_concurrency()는 정답이 아니라 시작점입니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
join()을 안 해도 프로그램 종료 시 정리된다 | joinable 상태 파괴 시 std::terminate() |
detach()는 성능 최적화 방법이다 | 추적/동기화/수명 관리가 어려워질 수 있음 |
| 스레드를 많이 만들수록 항상 빠르다 | 락 경합/컨텍스트 스위칭으로 느려질 수 있음 |
체크 질문 (스스로 답해보기)
std::thread 객체 소멸 시 왜 joinable() 상태가 위험한가?
- 참조 인자 전달에서
std::ref가 필요한 이유는 무엇인가?
- 요청마다 스레드를 만드는 방식보다 워커 풀이 유리한 이유는 무엇인가?