1 분 소요

간단히 스레드를 만들어 테스트해야 할 일이 있어 std::thread를 찾아보고 만들어봤다.

예전에는 boost::thread를 이용했었는데 C++11에 오면서 아예 다 표준에 들어왔다는 게 놀랍고 신기하다.

2026년 수정 안내 원래 실었던 예제에 문제가 세 가지 있었다. (1) char* target_msg = "test";는 C++11부터 컴파일되지 않는다. (2) 두 스레드가 동기화 없이 같은 전역 버퍼에 strcpy_s를 하는 데이터 레이스 코드였다. (3) while(true) 때문에 join()이 영원히 반환되지 않아 프로그램이 끝나지 않았다. 스레드 글에 실을 예제로 적절하지 않아 다시 작성한다.

기본 사용법

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

void CallbackFunction( int thread_num )
{
    for ( int i = 0; i < 5; ++i )
    {
        std::cout << "working (" << thread_num << ")\n";
        std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
    }
}

int main()
{
    std::thread thread1( CallbackFunction, 1 );
    std::thread thread2( CallbackFunction, 2 );

    thread1.join();
    thread2.join();

    return 0;
}

std::this_thread::sleep_for를 쓰면 windows.hSleep이 필요 없다. 표준이라 리눅스에서도 그대로 빌드된다.

join()은 해당 스레드가 끝날 때까지 기다린다. 따라서 스레드 함수는 언젠가 반환되어야 한다. 무한 루프를 돌아야 한다면 종료 플래그를 두고 빠져나올 수 있게 만들어야 한다.

공유 데이터에는 반드시 동기화가 필요하다

여러 스레드가 같은 버퍼에 동시에 쓰면 데이터 레이스가 되고, 이는 정의되지 않은 동작이다. 운 좋게 동작하는 것처럼 보이다가 부하가 걸리면 깨진다.

#include <mutex>
#include <string>

std::mutex  g_msg_mutex;
std::string g_msg;

void CallbackFunction( int thread_num )
{
    for ( int i = 0; i < 1000; ++i )
    {
        std::lock_guard<std::mutex> lock( g_msg_mutex );
        g_msg = "test " + std::to_string( thread_num );
    }
}

std::string을 쓰면 버퍼 크기를 직접 관리할 필요도 없어진다.

종료 플래그를 두는 패턴

#include <atomic>

std::atomic<bool> g_running{ true };

void WorkerFunction()
{
    while ( g_running.load( std::memory_order_relaxed ) )
    {
        // ... 작업 ...
        std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
    }
}

int main()
{
    std::thread worker( WorkerFunction );

    // ... 잠시 후 ...
    g_running = false;
    worker.join();

    return 0;
}

플래그를 bool이 아니라 std::atomic<bool>로 둔 것이 중요하다. 일반 bool에 여러 스레드가 접근하면 그 자체가 데이터 레이스이고, 컴파일러가 루프 밖으로 읽기를 끌어내 무한 루프가 되기도 한다.

join을 잊으면 프로그램이 죽는다

std::thread 객체가 소멸될 때 아직 join도 detach도 되지 않았다면 std::terminate()가 호출된다. 예외가 던져지는 경로까지 고려하면 실수하기 쉬운 부분이다.

C++20부터는 std::jthread 가 있다. 소멸자에서 알아서 join해주고, 협조적 종료를 위한 stop_token까지 제공한다.

#include <thread>

void WorkerFunction( std::stop_token token )
{
    while ( !token.stop_requested() )
    {
        // ... 작업 ...
    }
}

int main()
{
    std::jthread worker( WorkerFunction );
    return 0;      // 소멸자가 stop 요청 후 join까지 해준다
}

참고자료

댓글 남기기