C++ jthread | "자동 조인 스레드" 가이드

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::threadstd::jthread
자동 조인❌ (수동 필요)✅ (소멸자에서)
중단 메커니즘✅ (stop_token)
RAII
C++ 버전C++11C++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;
}

정리

핵심 요약

  1. 자동 조인: 소멸자에서 자동으로 join()
  2. stop_token: 협력적 중단 메커니즘
  3. RAII: 예외 안전성 보장
  4. stop_callback: 중단 시 콜백 실행
  5. std::thread 대체: C++20에서 jthread 권장

std::thread vs std::jthread

특징std::threadstd::jthread
조인수동 (join())자동 (소멸자)
중단없음stop_token
예외 안전성낮음높음 (RAII)
사용 편의성보통높음
C++ 버전C++11C++20

실전 팁

  1. 중단 메커니즘

    • 주기적으로 stop_requested() 확인
    • 긴 작업은 중간에 체크 포인트 추가
    • stop_callback으로 정리 작업 수행
  2. 성능

    • jthread와 thread의 성능은 동일
    • 자동 조인으로 코드가 간결해짐
    • 예외 안전성으로 버그 감소
  3. 마이그레이션

    • std::threadstd::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) 핵심 문법 치트시트 | 현업에서 자주 쓰는 한눈에 보기