본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : std::thread (11)

by snowoods 2025. 10. 17.

Modern C++

 

std::thread

 

개요

std::thread는 C++11부터 표준 라이브러리에 추가된 기능으로, 프로그램 내에서 새로운 스레드를 생성하고 관리할 수 있게 해주는 클래스입니다. 이를 통해 병렬(Parallel) 또는 동시성(Concurrent) 프로그래밍을 구현하여 프로그램의 성능을 향상시키거나 응답성을 높일 수 있습니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++11: std::thread 및 관련 기능들이 <thread> 헤더에 처음 도입되었습니다.

 

내용 설명

std::thread 객체는 생성 시 인자로 전달된 함수(또는 호출 가능한 객체)를 새로운 스레드에서 실행합니다. std::thread의 주요 특징은 다음과 같습니다.

  1. 생성과 실행: std::thread 객체를 생성하면 즉시 새로운 스레드가 실행을 시작합니다. 첫 번째 인자로는 스레드에서 실행할 함수, 람다 표현식, 함수 객체 등을 전달하고, 이후 인자들은 해당 함수에 전달될 인자들입니다.
  2. 인자 전달: 스레드 함수에 인자를 전달할 때, 기본적으로 값에 의한 복사(copy) 또는 이동(move)이 일어납니다. 만약 참조(reference)로 인자를 전달하고 싶다면 std::ref를 사용해야 합니다. 예제 코드의 std::ref(outputs[i])가 바로 그 예시입니다.
  3. Join: join() 멤버 함수는 해당 스레드가 작업을 완료할 때까지 현재 스레드(주로 메인 스레드)를 대기시킵니다. join()을 호출하지 않고 std::thread 객체의 소멸자가 호출되면 프로그램이 비정상적으로 종료(std::terminate)되므로, 생성된 스레드는 반드시 join() 또는 detach()를 호출해야 합니다.
  4. Detach: detach() 멤버 함수는 스레드를 생성한 스레드로부터 분리합니다. detach된 스레드는 백그라운드에서 독립적으로 실행되며, 더 이상 join할 수 없습니다. detach된 스레드는 실행이 끝나면 스스로 자원을 해제합니다.
  5. 스레드 ID: std::this_thread::get_id() 함수를 통해 현재 실행 중인 스레드의 고유 ID를 얻을 수 있습니다.

 

예제 코드

#include <array>
#include <cstdint>
#include <iostream>
#include <numeric>
#include <thread>
#include <chrono>

namespace
{
constexpr auto NUM_THREADS = size_t{3U};
};

void worker(const std::int32_t input, std::int32_t &output)
{
    std::cout << "Called worker from Thread: " << std::this_thread::get_id()
              << '\n';

    output = input * 2;

    std::this_thread::sleep_for(std::chrono::seconds(20));
}

int main()
{
    auto inputs = std::array<std::int32_t, NUM_THREADS>{};
    std::iota(inputs.begin(), inputs.end(), 0);
    auto outputs = std::array<std::int32_t, NUM_THREADS>{};
    std::fill(outputs.begin(), outputs.end(), 0);

    std::cout << "Main Thread ID: " << std::this_thread::get_id() << '\n';

    std::array<std::thread, NUM_THREADS> threads;
    for (std::uint32_t i = 0; i < NUM_THREADS; ++i)
    {
        threads[i] = std::thread(worker, inputs[i], std::ref(outputs[i]));
    }

    // ...

    for (std::uint32_t i = 0; i < NUM_THREADS; ++i)
    {
        threads[i].join();
    }

    for (std::uint32_t i = 0; i < NUM_THREADS; ++i)
    {
        std::cout << "Outputs[" << i << "] = " << outputs[i] << '\n';
    }

    return 0;
}

 

실행 결과

Main Thread ID: 140735252825920
Called worker from Thread: 140735252825921
Called worker from Thread: 140735252825922
Called worker from Thread: 140735252825923
// -- 20초 대기 --
Outputs[0] = 0
Outputs[1] = 2
Outputs[2] = 4

참고: 스레드 ID는 실행할 때마다 달라질 수 있으며, Called worker from Thread 메시지의 출력 순서는 스케줄링에 따라 보장되지 않습니다.

활용팁

  • RAII (Resource Acquisition Is Initialization) 패턴 활용: std::thread 객체가 소멸될 때 join()이나 detach()가 호출되도록 보장하는 래퍼(wrapper) 클래스를 만들면 예외 발생 시에도 안전하게 스레드 자원을 관리할 수 있습니다.
  • std::thread::hardware_concurrency(): 이 함수는 시스템이 지원하는 동시 스레드의 수를 반환합니다. 스레드 풀(thread pool)의 크기를 결정하는 등 최적화에 유용하게 사용할 수 있습니다. (단, 이 값은 힌트일 뿐이며 0을 반환할 수도 있습니다.)
  • 데이터 경쟁(Data Race) 주의: 여러 스레드가 공유 데이터에 동시에 접근할 때는 데이터 경쟁이 발생할 수 있습니다. 예제에서는 각 스레드가 outputs 배열의 서로 다른 원소에만 접근하므로 안전하지만, 공유 자원에 쓰기 작업을 할 때는 std::mutex, std::atomic 등을 사용하여 동기화를 해야 합니다.