C++ jthread | "자동 조인 스레드" 가이드
이 글의 핵심
C++ jthread에 대한 실전 가이드입니다.
들어가며
C++20의 std::jthread는 자동 조인과 중단 메커니즘을 제공하는 개선된 스레드 클래스입니다. RAII 원칙을 따라 안전하고 편리합니다.
1. jthread 기본
std::thread vs std::jthread
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
// std::thread: 수동 조인 필요
void useThread() {
std::thread t( {
std::cout << "std::thread 작업" << std::endl;
});
t.join(); // 필수! 없으면 std::terminate 호출
}
// std::jthread: 자동 조인
void useJthread() {
std::jthread jt( {
std::cout << "std::jthread 작업" << std::endl;
});
// 소멸자에서 자동으로 조인됨
}
int main() {
useThread();
useJthread();
}
비교표
| 특징 | std::thread | std::jthread |
|---|---|---|
| 자동 조인 | ❌ (수동 필요) | ✅ (소멸자에서) |
| 중단 메커니즘 | ❌ | ✅ (stop_token) |
| RAII | ❌ | ✅ |
| C++ 버전 | C++11 | C++20 |
핵심 개념:
- RAII: 소멸자에서 자동으로 리소스 정리
- 안전성: join() 누락으로 인한 terminate 방지
- 편의성: 명시적 조인 불필요
2. 기본 사용
간단한 예제
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
void simpleTask() {
std::cout << "작업 시작" << std::endl;
std::this_thread::sleep_for(1s);
std::cout << "작업 완료" << std::endl;
}
int main() {
std::cout << "메인 시작" << std::endl;
{
std::jthread t(simpleTask);
std::cout << "스레드 생성됨" << std::endl;
// 스코프를 벗어나면 자동 조인
}
std::cout << "메인 종료" << std::endl;
}
매개변수 전달
#include <iostream>
#include <thread>
void printNumbers(int start, int end) {
for (int i = start; i <= end; i++) {
std::cout << i << " ";
}
std::cout << std::endl;
}
int main() {
std::jthread t1(printNumbers, 1, 5);
std::jthread t2(printNumbers, 10, 15);
// 자동 조인됨
}
3. stop_token으로 중단하기
기본 중단 메커니즘
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
void worker(std::stop_token stoken) {
int count = 0;
while (!stoken.stop_requested()) {
std::cout << "작업 중... (" << count++ << ")" << std::endl;
std::this_thread::sleep_for(100ms);
}
std::cout << "중단됨" << std::endl;
}
int main() {
std::jthread t(worker);
std::this_thread::sleep_for(1s);
std::cout << "중단 요청" << std::endl;
t.request_stop(); // 중단 요청
// 자동 조인 (소멸자에서)
}
stop_token 활용
#include <iostream>
#include <thread>
#include <chrono>
#include <atomic>
using namespace std::chrono_literals;
void dataProcessor(std::stop_token stoken) {
std::atomic<int> processed{0};
while (!stoken.stop_requested()) {
// 데이터 처리
processed++;
// 주기적으로 중단 확인
if (processed % 100 == 0) {
std::cout << "처리된 데이터: " << processed << std::endl;
}
std::this_thread::sleep_for(10ms);
}
std::cout << "최종 처리량: " << processed << std::endl;
}
int main() {
std::jthread t(dataProcessor);
std::this_thread::sleep_for(2s);
t.request_stop();
}
4. 실전 예제
예제 1: RAII 패턴
#include <iostream>
#include <thread>
#include <stdexcept>
#include <chrono>
using namespace std::chrono_literals;
void riskyOperation() {
std::jthread worker( {
for (int i = 0; i < 10; i++) {
std::cout << "작업 " << i << std::endl;
std::this_thread::sleep_for(100ms);
}
});
// 예외가 발생해도 worker는 자동으로 조인됨
if (rand() % 2 == 0) {
throw std::runtime_error("예외 발생!");
}
std::cout << "정상 종료" << std::endl;
}
int main() {
try {
riskyOperation();
} catch (const std::exception& e) {
std::cout << "예외 처리: " << e.what() << std::endl;
}
std::cout << "메인 종료" << std::endl;
}
예제 2: 여러 스레드 관리
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
using namespace std::chrono_literals;
void workerTask(int id, std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "스레드 " << id << " 작업 중" << std::endl;
std::this_thread::sleep_for(200ms);
}
std::cout << "스레드 " << id << " 종료" << std::endl;
}
int main() {
std::vector<std::jthread> threads;
// 5개 스레드 생성
for (int i = 0; i < 5; i++) {
threads.emplace_back(workerTask, i);
}
std::cout << "모든 스레드 실행 중..." << std::endl;
std::this_thread::sleep_for(2s);
std::cout << "모든 스레드 중단 요청" << std::endl;
// 모든 스레드에 중단 요청
for (auto& t : threads) {
t.request_stop();
}
// 벡터 소멸 시 자동으로 모든 스레드 조인
std::cout << "메인 종료" << std::endl;
}
예제 3: 조건 변수와 함께 사용
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
using namespace std::chrono_literals;
std::mutex mtx;
std::condition_variable_any cv;
std::queue<int> taskQueue;
void worker(std::stop_token stoken) {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// stop_token을 지원하는 wait
if (cv.wait(lock, stoken, []{ return !taskQueue.empty(); })) {
int task = taskQueue.front();
taskQueue.pop();
lock.unlock();
std::cout << "처리: " << task << std::endl;
std::this_thread::sleep_for(100ms);
}
if (stoken.stop_requested()) {
std::cout << "워커 종료" << std::endl;
break;
}
}
}
int main() {
std::jthread t(worker);
// 작업 추가
for (int i = 0; i < 10; i++) {
{
std::lock_guard<std::mutex> lock(mtx);
taskQueue.push(i);
}
cv.notify_one();
std::this_thread::sleep_for(50ms);
}
std::this_thread::sleep_for(2s);
t.request_stop();
cv.notify_one(); // 대기 중인 스레드 깨우기
}
5. stop_token 고급 활용
stop_callback
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
void worker(std::stop_token stoken) {
// 중단 요청 시 콜백 등록
std::stop_callback callback(stoken, {
std::cout << "중단 콜백 호출됨!" << std::endl;
});
int count = 0;
while (!stoken.stop_requested()) {
std::cout << "작업 " << count++ << std::endl;
std::this_thread::sleep_for(200ms);
}
}
int main() {
std::jthread t(worker);
std::this_thread::sleep_for(1s);
t.request_stop(); // 콜백이 즉시 호출됨
}
stop_source
#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
void worker(std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "작업 중..." << std::endl;
std::this_thread::sleep_for(200ms);
}
}
int main() {
std::stop_source ssource;
std::stop_token stoken = ssource.get_token();
std::jthread t(worker, stoken);
std::this_thread::sleep_for(1s);
ssource.request_stop(); // stop_source로 중단 요청
}
6. 자주 발생하는 문제
문제 1: 조인 누락 (std::thread)
#include <thread>
#include <iostream>
// ❌ std::thread: 조인 누락 시 terminate
void badExample() {
std::thread t( {
std::cout << "작업" << std::endl;
});
// join()이나 detach() 없이 소멸
// → std::terminate 호출!
}
// ✅ std::thread: 명시적 조인
void goodExample1() {
std::thread t( {
std::cout << "작업" << std::endl;
});
t.join(); // 필수
}
// ✅ std::jthread: 자동 조인
void goodExample2() {
std::jthread t( {
std::cout << "작업" << std::endl;
});
// 소멸자에서 자동 조인
}
문제 2: 중단 체크 누락
#include <thread>
#include <iostream>
#include <chrono>
using namespace std::chrono_literals;
// ❌ 중단 체크 안함 (무한 루프)
void badWorker(std::stop_token stoken) {
while (true) {
std::cout << "작업 중..." << std::endl;
std::this_thread::sleep_for(100ms);
// stop_requested() 체크 없음!
}
}
// ✅ 주기적으로 중단 체크
void goodWorker(std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "작업 중..." << std::endl;
std::this_thread::sleep_for(100ms);
}
std::cout << "정상 종료" << std::endl;
}
문제 3: detach 사용
#include <thread>
#include <iostream>
int main() {
std::jthread t( {
std::cout << "작업" << std::endl;
});
// ❌ detach 후에는 자동 조인 안됨
t.detach();
// 스레드가 백그라운드에서 실행
// 메인이 종료되면 스레드도 강제 종료될 수 있음
}
문제 4: 이동 의미론
#include <thread>
#include <iostream>
int main() {
std::jthread t1( {
std::cout << "작업" << std::endl;
});
// ✅ 이동 가능
std::jthread t2 = std::move(t1);
// t1은 더 이상 유효하지 않음
// t1.request_stop(); // 정의되지 않은 동작
// t2는 유효
t2.request_stop();
}
7. 실전 예제: 백그라운드 작업 관리자
#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <chrono>
using namespace std::chrono_literals;
class TaskManager {
public:
using Task = std::function<void(std::stop_token)>;
void addTask(Task task) {
threads.emplace_back(task);
}
void stopAll() {
std::cout << "모든 작업 중단 요청" << std::endl;
for (auto& t : threads) {
t.request_stop();
}
}
size_t activeCount() const {
return threads.size();
}
~TaskManager() {
std::cout << "TaskManager 소멸 (자동 조인)" << std::endl;
}
private:
std::vector<std::jthread> threads;
};
int main() {
TaskManager manager;
// 작업 1: 카운터
manager.addTask( {
int count = 0;
while (!stoken.stop_requested()) {
std::cout << "카운터: " << count++ << std::endl;
std::this_thread::sleep_for(300ms);
}
});
// 작업 2: 모니터
manager.addTask( {
while (!stoken.stop_requested()) {
std::cout << "모니터링..." << std::endl;
std::this_thread::sleep_for(500ms);
}
});
// 작업 3: 로거
manager.addTask( {
while (!stoken.stop_requested()) {
std::cout << "로그 기록" << std::endl;
std::this_thread::sleep_for(1s);
}
});
std::cout << "활성 작업: " << manager.activeCount() << "개" << std::endl;
std::this_thread::sleep_for(3s);
manager.stopAll();
std::this_thread::sleep_for(1s);
std::cout << "메인 종료" << std::endl;
}
정리
핵심 요약
- 자동 조인: 소멸자에서 자동으로 join()
- stop_token: 협력적 중단 메커니즘
- RAII: 예외 안전성 보장
- stop_callback: 중단 시 콜백 실행
- std::thread 대체: C++20에서 jthread 권장
std::thread vs std::jthread
| 특징 | std::thread | std::jthread |
|---|---|---|
| 조인 | 수동 (join()) | 자동 (소멸자) |
| 중단 | 없음 | stop_token |
| 예외 안전성 | 낮음 | 높음 (RAII) |
| 사용 편의성 | 보통 | 높음 |
| C++ 버전 | C++11 | C++20 |
실전 팁
-
중단 메커니즘
- 주기적으로
stop_requested()확인 - 긴 작업은 중간에 체크 포인트 추가
stop_callback으로 정리 작업 수행
- 주기적으로
-
성능
- jthread와 thread의 성능은 동일
- 자동 조인으로 코드가 간결해짐
- 예외 안전성으로 버그 감소
-
마이그레이션
std::thread→std::jthread로 쉽게 전환- stop_token 매개변수 추가로 중단 지원
- 기존 join() 호출 제거
다음 단계
- C++ stop_token
- C++ thread_local
- C++ Multithreading Basics
관련 글
- C++ stop_token |
- C++ Barrier & Latch |
- C++ Branch Prediction |
- C++ Calendar & Timezone |
- 모던 C++ (C++11~C++20) 핵심 문법 치트시트 | 현업에서 자주 쓰는 한눈에 보기