C++20 std::jthread: 자동 join과 stop_token으로 스레드 협조적 중단하기
이 글의 핵심
std::thread는 join이나 detach를 빠뜨린 채 소멸되면 std::terminate가 호출되어 예외 경로에서 특히 위험합니다. jthread는 이를 RAII로 막아 주지만, 루프 안에서 stop_requested()를 확인하지 않으면 중단 요청이 전달되지 않습니다. 조건 변수와 함께 쓰는 법과 백그라운드 작업 관리자 예제까지 따라가 볼 수 있습니다.
들어가며
C++20의 std::jthread는 자동 조인과 중단 메커니즘을 제공하는 개선된 스레드 클래스입니다. RAII 원칙을 따라 안전하고 편리합니다.
join을 잊어도 terminate되지 않는 스레드
std::thread를 실무에서 다뤄본 사람이라면 한 번쯤 join()을 빼먹고 프로그램이 std::terminate로 죽는 경험을 했을 것입니다. 예외가 던져지거나 조기 반환 경로가 늘어나면 모든 실행 경로에서 join() 또는 detach()를 빠짐없이 호출하기가 생각보다 어렵습니다. std::jthread는 이 문제를 소멸자에서 자동으로 join()을 호출하는 방식으로 해결하고, 여기에 더해 협력적 중단(cooperative cancellation)을 위한 stop_token 메커니즘까지 표준으로 제공합니다.
스코프를 벗어날 때 std::thread와 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 방지
- 편의성: 명시적 조인 불필요
블록 스코프로 수명 관리하기
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);
// 자동 조인됨
}
stop_token으로 협력적 중단하기
std::jthread가 자동 조인만 제공했다면 그 자체로는 큰 의미가 없었을 것입니다. 진짜 가치는 stop_token을 통한 협력적 중단에 있습니다. 기존 std::thread로 스레드를 중간에 멈추려면 std::atomic<bool> 플래그를 직접 만들어 워커 함수와 공유해야 했지만, jthread는 이 패턴을 표준 라이브러리 차원에서 대신 처리해 줍니다.
첫 매개변수로 stop_token 받기
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_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();
}
stop_callback과 stop_source
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로 중단 요청
}
jthread를 써도 남는 함정
jthread가 여러 문제를 자동으로 해결해 준다고 해서 모든 실수가 사라지는 것은 아닙니다. 아래는 실무에서 실제로 자주 마주치는 네 가지 함정으로, 각각 std::thread와의 비교를 통해 무엇이 달라지고 무엇은 여전히 개발자가 신경 써야 하는지를 보여줍니다.
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;
});
// 소멸자에서 자동 조인
}
워커가 stop_requested()를 확인하지 않음
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;
}
detach로 자동 조인을 포기함
jthread에서도 detach()를 호출하는 것은 여전히 가능하지만, 그 순간 자동 조인이라는 핵심 이점을 스스로 포기하는 셈이 됩니다. detach()된 스레드는 더 이상 소유자와 연결되지 않고 완전히 독립적으로 실행되므로, 메인 프로그램이 먼저 종료되면 분리된 스레드는 정리되지 못한 채 강제로 죽거나 아예 실행 도중 끊길 수 있습니다. 로그 파일 flush나 네트워크 연결 정리처럼 마무리 작업이 필요한 스레드라면 detach() 대신 jthread의 기본 동작인 자동 조인을 그대로 활용하는 편이 안전합니다.
#include <thread>
#include <iostream>
int main() {
std::jthread t([] {
std::cout << "작업" << std::endl;
});
// ❌ detach 후에는 자동 조인 안 됨
t.detach();
// 스레드가 백그라운드에서 실행
// 메인이 종료되면 스레드도 강제 종료될 수 있음
}
이동한 뒤의 jthread 객체
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();
}
예외·여러 워커·조건 변수와 함께 쓰기
이론만으로는 jthread의 장점이 잘 와닿지 않을 수 있으므로, 예외 처리·다중 스레드 관리·조건 변수 연동이라는 세 가지 실전 시나리오를 통해 자동 조인과 stop_token이 실제 코드에서 어떻게 결합되는지 살펴보겠습니다.
예외가 나도 조인되는 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;
}
vector로 워커 여러 개 관리
여러 개의 워커 스레드를 동시에 다뤄야 한다면 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;
}
condition_variable_any와 stop_token 연동
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(); // 대기 중인 스레드 깨우기
}
백그라운드 작업 관리자 만들기
지금까지 배운 자동 조인과 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;
}
jthread 정리
std::jthread는 단순히 std::thread에 이름 하나를 바꿔 붙인 클래스가 아니라, RAII 기반의 자동 조인과 협력적 중단 메커니즘을 표준 라이브러리 차원에서 제공하는 개선된 스레드 타입입니다. 아래 요약을 통해 지금까지 다룬 핵심 내용을 다시 정리합니다.
핵심 요약
- 자동 조인: 소멸자에서 자동으로 join()
- stop_token: 협력적 중단 메커니즘
- RAII: 예외 안전성 보장
- stop_callback: 중단 시 콜백 실행
- std::thread 대체: C++20에서 jthread 권장
std::thread와 std::jthread 비교
| 특징 | std::thread | std::jthread |
|---|---|---|
| 조인 | 수동 (join()) | 자동 (소멸자) |
| 중단 | 없음 | stop_token |
| 예외 안전성 | 낮음 | 높음 (RAII) |
| 사용 편의성 | 보통 | 높음 |
| C++ 버전 | C++11 | C++20 |
이어서 볼 글
같이 보면 좋은 글
- C++ stop_token
- C++ Barrier & Latch
- C++ Branch Prediction
- C++ Calendar & Timezone
- 모던 C++ (C++11~C++20) 핵심 문법 치트시트 | 현업에서 자주 쓰는 한눈에 보기
- C++ nullptr vs NULL: NULL이 정수라서 생기는 오버로드·템플릿 버그
- C++ Chrono Literals | ‘시간 리터럴’ 가이드
- C++ thread_local: 스레드별 카운터·버퍼, 초기화 시점과 소멸 순서
자주 묻는 질문 (FAQ)
Q. stop_callback은 어느 스레드에서 실행되나요?
A. std::stop_callback에 등록한 콜백은 request_stop()을 호출한 스레드에서 그 호출 안에서 동기적으로 실행됩니다. 콜백을 등록하는 시점에 이미 중단 요청이 들어와 있었다면, 등록하는 스레드에서 생성자 안에서 바로 실행됩니다. 따라서 콜백 안에서는 조건 변수 notify나 소켓 cancel처럼 짧은 작업만 하고, 오래 걸리는 작업이나 다른 락을 잡는 코드는 피해야 request_stop()을 부른 쪽이 멈추지 않습니다.