[모던 C++ #9] 직접 스레드 관리에서 std::jthread와 코루틴으로!

과거 C++98/03 시절, 멀티스레딩을 구현하기 위해서는 운영체제별 API(pthread, Win32 threads 등)를 직접 호출하거나, Boost Threads와 같은 서드파티 라이브러리에 의존해야 했습니다. 이는 코드 이식성을 저하시켰고, 스레드 생성, 종료, 동기화 관리가 번거롭게 이루어지는 경우가 많았습니다.

C++11 이후 표준 라이브러리에 std::thread, std::mutex, std::lock_guard, std::async 등 기본적인 멀티스레딩 기능이 도입되었고, C++20에서는 std::jthread와 중단 요청(stop token) 메커니즘이 추가되어 스레드 관리가 더 단순해졌습니다. 또한 C++20 코루틴(coroutine)을 활용하면 비동기 처리를 더 간결하고 직관적으로 구현할 수 있습니다.

관련 참고 자료:

과거: 운영체제별 스레드와 수동적 자원 관리

C++98/03 시절 멀티스레딩은 표준으로 지원되지 않아, 플랫폼별 API를 직접 호출하거나 Boost Threads 같은 서드파티 라이브러리에 의존해야 했습니다. 이 경우 스레드를 생성, 조인(join), 종료할 때 발생하는 예외 상황을 직접 처리하고, 안전한 종료를 보장하기 위해 많은 주의를 기울여야 했습니다.

// 가상 예제: POSIX pthread 사용(플랫폼 의존)
#include <pthread.h>
#include <iostream>

void* thread_func(void* arg) {
    std::cout << "Hello from pthread!\n";
    return nullptr;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_func, nullptr);
    pthread_join(tid, nullptr); // 수동 조인 필요
    return 0;
}

위 예제처럼 스레드 생성, 조인 시 플랫폼 별 함수를 호출하고, 에러 처리 로직도 직접 다루어야 했습니다.

현재: std::jthread와 코루틴

std::jthread: RAII 기반 스레드 관리

C++11에서 도입된 std::thread는 플랫폼 독립적인 스레드 인터페이스를 제공하지만, 스레드가 범위를 벗어날 때 자동으로 조인(join)하지는 않았습니다. C++20의 std::jthread는 RAII를 통해 스레드 종료를 자동 처리하며, 중단 요청(stop request) 메커니즘을 지원해 스레드 종료를 안전하고 간단하게 관리할 수 있습니다.

#include <iostream>
#include <thread>
#include <stop_token>

void worker(std::stop_token st) {
    while (!st.stop_requested()) {
        std::cout << "Working...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    std::cout << "Stopped!\n";
}

int main() {
    std::jthread jt(worker);
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    // 중단 요청
    jt.request_stop();
    // jt 소멸 시 자동으로 조인
    return 0;
}

std::jthread는 범위를 벗어날 때 자동으로 조인하므로 스레드 관리에 대한 부담이 줄어듭니다. 또한 request_stop()로 중단을 알리고, 작업 함수는 stop_requested()를 통해 안전하게 종료를 감지할 수 있습니다.

코루틴: 비동기 코드를 동기적 흐름처럼 작성

C++20 코루틴을 사용하면 비동기 처리를 더 직관적으로 표현할 수 있습니다. 기존에는 콜백이나 미래(std::future) 기반으로 비동기 코드를 작성해야 했지만, 코루틴을 통해 "비동기 함수 호출"을 마치 동기 함수처럼 기술할 수 있습니다.

#include <iostream>
#include <coroutine>
#include <chrono>
#include <thread>

struct my_task {
    struct promise_type {
        my_task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

my_task async_print() {
    std::cout << "Start\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    std::cout << "End\n";
    co_return;
}

int main() {
    // 이 예제는 단순한 코루틴 예시이며, 비동기 I/O나 이벤트 루프를 통해 더 실용적으로 활용 가능
    async_print(); 
    return 0;
}

위 예제는 단순한 예제지만, 코루틴을 활용하면 각종 비동기 I/O, 타이머, 이벤트 기반 로직을 동기 코드에 가깝게 표현할 수 있습니다. 이를 통해 비동기 로직이 복잡하게 얽힌 콜백 지옥(callback hell)을 탈출하고, 유지보수하기 쉬운 코드를 작성할 수 있습니다.

왜 이런 변화가 필요한가?

  1. 코드 이식성 및 단순화
    표준 라이브러리 기반 스레드(std::thread, std::jthread)를 사용하면 운영체제별 API를 직접 호출할 필요가 없고, RAII를 통해 스레드 종료 관리가 단순해집니다.
  2. 안전한 중단 메커니즘
    std::jthread와 중단 토큰(stop token)을 활용하면 스레드를 우아하게 종료하는 로직을 간단히 구현할 수 있습니다.
  3. 비동기 코드 가독성 향상
    C++20 코루틴을 통해 비동기 코드를 동기적으로 기술할 수 있어, 복잡한 상태 관리나 콜백 체인을 줄이고, 읽기 쉽고 유지보수하기 좋은 비동기 코드를 작성할 수 있습니다.
반응형