C++ jthread | '자동 조인 스레드' 가이드
이 글의 핵심
C++ jthread의 C++, jthread, "자동, 1.
들어가며
C++20의 std::jthread는 자동 조인과 중단 메커니즘을 제공하는 개선된 스레드 클래스입니다. RAII 원칙을 따라 안전하고 편리합니다.
1. jthread 기본
std::thread를 실무에서 다뤄본 사람이라면 한 번쯤 join()을 빼먹고 프로그램이 std::terminate로 죽는 경험을 했을 것입니다. 예외가 던져지거나 조기 반환 경로가 늘어나면 모든 실행 경로에서 join() 또는 detach()를 빠짐없이 호출하기가 생각보다 어렵습니다. std::jthread는 이 문제를 소멸자에서 자동으로 join()을 호출하는 방식으로 해결하고, 여기에 더해 협력적 중단(cooperative cancellation)을 위한 stop_token 메커니즘까지 표준으로 제공합니다.
std::thread vs std::jthread
두 클래스의 차이는 코드 몇 줄로도 바로 드러납니다. 아래 예제는 동일한 작업을 std::thread와 std::jthread로 각각 실행했을 때, 스코프를 벗어나는 시점에 어떤 일이 벌어지는지를 보여줍니다. useThread()에서는 t.join()을 명시적으로 호출하지 않으면 스레드 객체가 소멸될 때 std::terminate()가 호출되어 프로그램 전체가 강제 종료됩니다. 반면 useJthread()에서는 별도의 조인 호출이 없어도 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();
}
비교표
두 타입의 차이를 한눈에 정리하면 다음과 같습니다. 특히 “자동 조인”과 “중단 메커니즘” 두 항목이 jthread를 도입해야 하는 실질적인 이유입니다.
| 특징 | std::thread | std::jthread |
|---|---|---|
| 자동 조인 | ❌ (수동 필요) | ✅ (소멸자에서) |
| 중단 메커니즘 | ❌ | ✅ (stop_token) |
| RAII | ❌ | ✅ |
| C++ 버전 | C++11 | C++20 |
핵심 개념
- RAII: 소멸자에서 자동으로 리소스 정리
- 안전성: join() 누락으로 인한 terminate 방지
- 편의성: 명시적 조인 불필요
2. 기본 사용
jthread를 쓸 때 가장 먼저 익혀야 할 습관은 “블록 스코프로 수명을 관리한다”는 사고방식입니다. 스레드 객체를 중괄호 블록 안에 선언해두면, 블록을 벗어나는 순간 소멸자가 호출되면서 자동으로 조인이 일어나기 때문에 별도의 동기화 코드 없이도 “이 블록이 끝나면 스레드 작업도 끝나 있다”는 것을 보장할 수 있습니다.
간단한 예제
아래 예제는 simpleTask()를 별도 스레드에서 실행하면서, 내부 블록을 벗어나는 지점에서 자동 조인이 일어나는 흐름을 보여줍니다. “스레드 생성됨” 로그가 출력된 후에도 메인 스레드는 블록의 닫는 중괄호를 만날 때까지 대기하게 되므로, “메인 종료” 로그는 항상 simpleTask()가 완료된 다음에 출력됩니다.
#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;
}
매개변수 전달
std::jthread의 생성자는 std::thread와 마찬가지로 가변 인자를 받아 그대로 콜러블에 전달합니다. 즉 함수 포인터 뒤에 인자를 나열하기만 하면 되며, 별도의 래핑 없이 여러 스레드를 동시에 서로 다른 인자로 실행할 수 있습니다. 아래 예제처럼 t1, t2가 각각 다른 범위의 숫자를 출력하도록 만들 수 있고, 두 스레드 모두 블록이 끝날 때 자동으로 조인됩니다.
#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으로 중단하기
std::jthread가 자동 조인만 제공했다면 그 자체로는 큰 의미가 없었을 것입니다. 진짜 가치는 stop_token을 통한 협력적 중단에 있습니다. 기존 std::thread로 스레드를 중간에 멈추려면 std::atomic<bool> 플래그를 직접 만들어 워커 함수와 공유해야 했지만, jthread는 이 패턴을 표준 라이브러리 차원에서 대신 처리해 줍니다.
기본 중단 메커니즘
jthread의 생성자에 전달하는 함수가 첫 번째 매개변수로 std::stop_token을 받도록 선언하면, 런타임이 이를 자동으로 인식하고 연결해 줍니다. 워커 함수 내부에서는 stoken.stop_requested()를 주기적으로 검사하여 반복문을 빠져나가면 되고, 외부에서는 t.request_stop()을 호출하는 것만으로 중단 신호를 보낼 수 있습니다. 아래 예제에서 main()은 1초 대기 후 request_stop()을 호출하고, 이어서 스코프가 끝나며 자동 조인이 일어나 워커 스레드가 완전히 종료될 때까지 기다립니다.
#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 활용
실무에서는 단순히 반복문 조건에 stop_requested()를 넣는 것만으로 끝나지 않는 경우가 많습니다. 아래 dataProcessor() 예제처럼 처리량을 카운트하면서 일정 주기로 진행 상황을 로그로 남기고, 중단 요청이 들어오면 지금까지 처리한 총량을 마지막에 출력하는 패턴은 배치 작업이나 장시간 실행되는 워커에서 흔히 쓰입니다. 이렇게 하면 중단이 “갑자기 끊기는 것”이 아니라 “정상적으로 마무리되는 것”처럼 동작하게 됩니다.
#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. 실전 예제
이론만으로는 jthread의 장점이 잘 와닿지 않을 수 있으므로, 예외 처리·다중 스레드 관리·조건 변수 연동이라는 세 가지 실전 시나리오를 통해 자동 조인과 stop_token이 실제 코드에서 어떻게 결합되는지 살펴보겠습니다.
예제 1: RAII 패턴
std::thread를 쓸 때 가장 골치 아픈 상황 중 하나가 바로 예외 처리입니다. join()을 호출하기 전에 예외가 던져지면 스택 되감기(stack unwinding) 과정에서 스레드 객체가 조인되지 않은 채로 소멸되어 프로그램이 강제 종료됩니다. jthread는 소멸자가 항상 호출된다는 C++의 기본 원칙(RAII)을 그대로 활용하므로, 아래 예제처럼 throw 문이 실행되어 스택이 풀리더라도 worker 스레드는 안전하게 조인된 뒤에 예외 전파가 이어집니다.
#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: 여러 스레드 관리
여러 개의 워커 스레드를 동시에 다뤄야 한다면 std::vector<std::jthread>에 담아 관리하는 것이 가장 간단합니다. 각 스레드에 개별적으로 request_stop()을 호출해 중단 신호를 보낸 뒤, 벡터 자체가 스코프를 벗어나면 벡터의 소멸자가 각 원소의 소멸자를 순서대로 호출하면서 모든 스레드가 자동으로 조인됩니다. std::thread로 같은 구조를 만들려면 별도의 반복문에서 join()을 일일이 호출해야 하고, 그 반복문 이전에 예외가 발생하면 여전히 위험에 노출됩니다.
#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: 조건 변수와 함께 사용
stop_token은 C++20에서 함께 도입된 std::condition_variable_any::wait의 오버로드와 결합할 수 있다는 점에서 특히 유용합니다. 기존 조건 변수는 notify_one()으로 깨우지 않는 이상 무한정 대기하기 때문에, 중단 요청이 들어와도 스레드가 잠들어 있으면 즉시 반응하지 못하는 문제가 있었습니다. 아래 예제의 cv.wait(lock, stoken, predicate)처럼 stop_token을 함께 넘기면, 조건을 만족하거나 중단이 요청되는 두 경우 모두에 대해 대기가 풀리므로 생산자-소비자 패턴에서 안전하게 종료 처리를 할 수 있습니다.
#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_requested()를 폴링하는 방식 외에도, stop_token 생태계는 이벤트 기반으로 반응할 수 있는 두 가지 도구를 추가로 제공합니다. 하나는 중단 요청이 들어오는 순간 콜백을 실행하는 stop_callback이고, 다른 하나는 jthread와 독립적으로 중단 신호를 만들고 여러 대상에 뿌릴 수 있는 stop_source입니다.
stop_callback
stop_callback은 stop_token에 콜백을 등록해두면, 누군가 request_stop()을 호출하는 즉시(그리고 동기적으로) 해당 콜백이 실행되는 구조입니다. 폴링 루프 안에서 다음 반복까지 기다릴 필요 없이 리소스 정리나 로깅을 즉시 수행하고 싶을 때 유용합니다. 다만 콜백이 request_stop()을 호출한 스레드에서 곧바로 실행될 수 있으므로, 콜백 내부에서 락을 다시 거는 등 데드락을 유발할 수 있는 코드는 피해야 합니다.
#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
jthread가 자체적으로 가지고 있는 stop_token을 쓰는 대신, std::stop_source를 직접 만들어서 jthread 생성자에 전달할 수도 있습니다. 이렇게 하면 하나의 stop_source에서 얻은 여러 개의 stop_token을 서로 다른 스레드에 나눠 줄 수 있어, 여러 스레드를 하나의 중단 신호로 한꺼번에 멈추는 팬아웃(fan-out) 구조를 만들 때 유리합니다.
#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. 자주 발생하는 문제
jthread가 여러 문제를 자동으로 해결해 준다고 해서 모든 실수가 사라지는 것은 아닙니다. 아래는 실무에서 실제로 자주 마주치는 네 가지 함정으로, 각각 std::thread와의 비교를 통해 무엇이 달라지고 무엇은 여전히 개발자가 신경 써야 하는지를 보여줍니다.
문제 1: 조인 누락 (std::thread)
std::thread의 가장 대표적인 함정은 join()이나 detach() 없이 스레드 객체가 스코프를 벗어나는 경우입니다. 이 경우 소멸자 내부에서 joinable()이 참이면 표준이 std::terminate()를 호출하도록 강제하고 있어, 디버깅 로그 한 줄 없이 프로세스 전체가 죽어버립니다. jthread로 바꾸기만 해도 이 클래스의 소멸자가 자동으로 request_stop()과 join()을 함께 처리하므로 이런 종류의 크래시 자체가 원천적으로 사라집니다.
#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: 중단 체크 누락
jthread가 소멸자에서 request_stop()을 자동으로 호출해 주긴 하지만, 워커 함수 내부가 stop_requested()를 전혀 검사하지 않는다면 중단 신호는 그냥 무시됩니다. 아래 badWorker()처럼 while (true)로만 작성된 루프는 중단 요청이 들어와도 절대 끝나지 않으므로, 소멸자에서의 join()이 영원히 대기하는 상황(사실상 데드락)이 벌어집니다. 따라서 중단 가능한 워커를 작성할 때는 반복문 조건이나 각 반복 시작 지점에서 반드시 stop_requested()를 확인하는 습관을 들여야 합니다.
#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 사용
jthread에서도 detach()를 호출하는 것은 여전히 가능하지만, 그 순간 자동 조인이라는 핵심 이점을 스스로 포기하는 셈이 됩니다. detach()된 스레드는 더 이상 소유자와 연결되지 않고 완전히 독립적으로 실행되므로, 메인 프로그램이 먼저 종료되면 분리된 스레드는 정리되지 못한 채 강제로 죽거나 아예 실행 도중 끊길 수 있습니다. 로그 파일 flush나 네트워크 연결 정리처럼 마무리 작업이 필요한 스레드라면 detach() 대신 jthread의 기본 동작인 자동 조인을 그대로 활용하는 편이 안전합니다.
#include <thread>
#include <iostream>
int main() {
std::jthread t([] {
std::cout << "작업" << std::endl;
});
// ❌ detach 후에는 자동 조인 안 됨
t.detach();
// 스레드가 백그라운드에서 실행
// 메인이 종료되면 스레드도 강제 종료될 수 있음
}
문제 4: 이동 의미론
std::jthread는 std::thread와 마찬가지로 복사할 수 없고 이동만 가능한 타입입니다. std::move로 소유권을 다른 객체로 넘기고 나면 원래 객체는 더 이상 실행 중인 스레드를 참조하지 않는 빈 상태가 되므로, 그 상태에서 request_stop()과 같은 멤버 함수를 호출하는 것은 정의되지 않은 동작으로 이어질 수 있습니다. 컨테이너에 jthread를 담거나 함수에서 반환할 때 이동이 자주 일어나므로, “이동 후에는 원본을 건드리지 않는다”는 원칙을 지켜야 합니다.
#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. 실전 예제: 백그라운드 작업 관리자
지금까지 배운 자동 조인과 stop_token을 하나로 묶어, 여러 개의 백그라운드 작업을 등록하고 한꺼번에 중단시킬 수 있는 작은 TaskManager 클래스를 만들어 보겠습니다. 이런 구조는 서버 애플리케이션에서 헬스체크 스레드, 로그 플러셔, 메트릭 수집기처럼 여러 개의 상시 실행 작업을 관리해야 할 때 실제로 자주 쓰이는 패턴입니다. TaskManager는 내부적으로 std::vector<std::jthread>만 들고 있으면 되고, stopAll()로 모든 작업에 중단 신호를 보낸 뒤에는 벡터의 소멸자가 나머지 조인을 알아서 처리해 줍니다.
#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([](std::stop_token stoken) {
int count = 0;
while (!stoken.stop_requested()) {
std::cout << "카운터: " << count++ << std::endl;
std::this_thread::sleep_for(300ms);
}
});
// 작업 2: 모니터
manager.addTask([](std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "모니터링..." << std::endl;
std::this_thread::sleep_for(500ms);
}
});
// 작업 3: 로거
manager.addTask([](std::stop_token stoken) {
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;
}
정리
std::jthread는 단순히 std::thread에 이름 하나를 바꿔 붙인 클래스가 아니라, RAII 기반의 자동 조인과 협력적 중단 메커니즘을 표준 라이브러리 차원에서 제공하는 개선된 스레드 타입입니다. 아래 요약을 통해 지금까지 다룬 핵심 내용을 다시 정리합니다.
핵심 요약
- 자동 조인: 소멸자에서 자동으로 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++ Barrier & Latch
- C++ Branch Prediction
- C++ Calendar & Timezone
- 모던 C++ (C++11~C++20) 핵심 문법 치트시트 | 현업에서 자주 쓰는 한눈에 보기
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Everything about C++ jthread : from basic concepts to practical applications.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ nullptr vs NULL | ‘널 포인터’ 가이드
- C++ Chrono Literals | ‘시간 리터럴’ 가이드
- C++ thread_local | ‘스레드 로컬 저장소’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, jthread, thread, C++20, RAII 등으로 검색하시면 이 글이 도움이 됩니다.