C++에서의 표준 스레드 프로그래밍: 동시성 제어와 동기화 기법

표준 라이브러리를 활용한 멀티스레딩

C++11부터 제공되는 표준 스레드 라이브러리는 플랫폼에 독립적인 동시성 프로그래밍을 가능하게 한다. 필요한 헤더는 <thread>, <mutex>, <atomic>, <condition_variable> 등이며, 이들을 조합해 스레드 생성, 상호배제, 동기화, 원자적 연산 등을 구현할 수 있다.

스레드 생성과 실행 생명주기 관리

std::thread 객체를 생성하면 즉시 새로운 실행 흐름이 시작된다. 주요 두 가지 정책으로 자식 스레드의 종료를 처리할 수 있다:
  • join(): 호출 스레드가 해당 스레드의 종료를 명시적으로 대기한다.
  • detach(): 스레드를 분리 상태로 전환하여 독립적으로 실행되게 하며, 종료 시 자동 정리된다.
분리된 스레드는 부모가 종료되면 전체 프로세스가 종료되면서 함께 종료된다.
#include <iostream>
#include <thread>
#include <chrono>

void worker() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "작업 스레드 실행 완료\n";
}

int main() {
    std::thread t(worker);
    t.join(); // 메인 스레드가 t 종료까지 대기
    std::cout << "메인 스레드 종료\n";
    return 0;
}

공유 자원 보호: 뮤텍스와 잠금 관리

여러 스레드가 동시에 접근하는 공유 변수는 데이터 경합(race condition)을 일으킬 수 있다. 이를 방지하기 위해 std::mutex와 함께 RAII 기반의 잠금 클래스를 사용한다. 예를 들어, 세 개의 티켓 판매 창구가 동시에 티켓 수량을 감소시키는 상황에서, 임계 영역(critical section)을 보호해야 한다.
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

int remainingTickets = 50;
std::mutex ticketMutex;

void sellTicket(int windowId) {
    while (true) {
        {
            std::lock_guard<std::mutex> lock(ticketMutex);
            if (remainingTickets > 0) {
                std::cout << "창구 " << windowId 
                          << ": " << remainingTickets-- 
                          << "번 티켓 판매\n";
            } else {
                break;
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

int main() {
    std::vector<std::thread> windows;
    for (int i = 1; i <= 3; ++i) {
        windows.emplace_back(sellTicket, i);
    }

    for (auto& w : windows) {
        w.join();
    }
    std::cout << "모든 티켓 판매 완료\n";
    return 0;
}

스레드 간 동기화: 조건 변수

뮤텍스는 배제만을 제공하지만, 특정 조건이 충족될 때까지 스레드를 대기시키려면 std::condition_variable이 필요하다. 생산자-소비자 패턴에서 유용하게 사용된다. 다음은 큐가 비었을 때 소비자가 대기하고, 생산자가 항목을 넣으면 소비자를 깨우는 예제이다.
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>

class ThreadSafeQueue {
private:
    std::queue<int> dataQueue;
    mutable std::mutex queueMutex;
    std::condition_variable itemAvailable;

public:
    void produce(int value) {
        std::unique_lock<std::mutex> lock(queueMutex);
        itemAvailable.wait(lock, [this]() { return dataQueue.empty(); });
        dataQueue.push(value);
        std::cout << "[생산] " << value << "번 아이템 생성\n";
        itemAvailable.notify_all();
    }

    int consume() {
        std::unique_lock<std::mutex> lock(queueMutex);
        itemAvailable.wait(lock, [this]() { return !dataQueue.empty(); });
        int value = dataQueue.front();
        dataQueue.pop();
        std::cout << "[소비] " << value << "번 아이템 처리\n";
        itemAvailable.notify_all();
        return value;
    }
};

void producerWorker(ThreadSafeQueue* q) {
    for (int i = 1; i <= 5; ++i) {
        q->produce(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

void consumerWorker(ThreadSafeQueue* q) {
    for (int i = 1; i <= 5; ++i) {
        q->consume();
        std::this_thread::sleep_for(std::chrono::milliseconds(250));
    }
}

int main() {
    ThreadSafeQueue queue;
    std::thread p(producerWorker, &queue);
    std::thread c(consumerWorker, &queue);

    p.join();
    c.join();
    return 0;
}

std::unique_lockstd::lock_guard보다 유연하며, 조건 변수와 함께 사용할 수 있고, 잠금 해제/획득을 수동으로 제어할 수 있다.

무임장 연산: std::atomic을 통한 성능 최적화

뮤텍스는 커널 리소스를 사용하므로 오버헤드가 크다. 단순한 증감 연산 같은 경우 std::atomic을 사용하면 CAS(compare-and-swap) 기반의 무임장(임계영역 없음) 동기화가 가능하다.
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>

std::atomic<bool> ready{false};
std::atomic<int> counter{0};

void workerTask() {
    while (!ready) {
        std::this_thread::yield(); // CPU 양보
    }
    for (int i = 0; i < 1000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::vector<std::thread> workers;
    for (int i = 0; i < 10; ++i) {
        workers.emplace_back(workerTask);
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    ready = true;

    for (auto& w : workers) {
        w.join();
    }
    std::cout << "최종 카운터 값: " << counter.load() << "\n";
    return 0;
}

volatile은 C++에서 메모리 가시성 보장을 위해 사용되지 않으며, 대신 std::atomic과 메모리 순서 지정자(memory order)를 사용해야 한다.

태그: Multithreading C++ std::thread std::mutex std::condition_variable

7월 1일 19:39에 게시됨