[2026] C++ jthread | 자동 조인 스레드 가이드
이 글의 핵심
C++ jthread의 C++, jthread, 자동, 1.
들어가며
C++20의 std::jthread는 자동 조인과 중단 메커니즘을 제공하는 개선된 스레드 클래스입니다. RAII 원칙을 따라 안전하고 편리합니다.
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
1. jthread 기본
std::thread vs std::jthread
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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. 기본 사용
간단한 예제
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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;
}
매개변수 전달
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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으로 중단하기
기본 중단 메커니즘
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 활용
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 패턴
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 여러 스레드 관리
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 조건 변수와 함께 사용
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 중단 체크 누락
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 사용
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <thread>
#include <iostream>
int main() {
std::jthread t( {
std::cout << "작업" << std::endl;
});
// ❌ detach 후에는 자동 조인 안됨
t.detach();
// 스레드가 백그라운드에서 실행
// 메인이 종료되면 스레드도 강제 종료될 수 있음
}
문제 4: 이동 의미론
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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. 실전 예제: 백그라운드 작업 관리자
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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() 호출 제거