Programming/C++

C++ : 스레드와 동기화

나무수피아 2025. 5. 23. 01:17
728x90
반응형
C++ 스레드와 동기화

스레드와 동기화

1. std::thread, std::mutex, std::lock_guard

C++11에서는 병렬 프로그래밍을 지원하기 위해 std::thread, std::mutex, std::lock_guard와 같은 다양한 도구들을 표준 라이브러리에 포함시켰습니다. 이들은 멀티스레드 환경에서 안정적인 데이터 처리를 가능하게 하며, 자원 경쟁(Race Condition)을 방지하는 데 중요한 역할을 합니다.

1.1. std::thread

std::thread는 독립적으로 실행되는 코드 단위를 생성하는 데 사용됩니다. 하나의 프로세스 내에서 여러 개의 스레드를 생성하여, 각기 다른 작업을 동시에 처리할 수 있게 해줍니다. 이를 통해 프로그램의 반응성을 높이고, 병렬 처리를 통해 성능을 향상시킬 수 있습니다.

#include <iostream>
#include <thread>

void printMessage() {
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    std::thread t(printMessage);  // 스레드 생성
    t.join();  // 스레드가 종료될 때까지 기다림
    return 0;
}

스레드를 생성한 후에는 join()이나 detach()를 호출해야 합니다. join()은 해당 스레드가 종료될 때까지 기다리는 반면, detach()는 스레드를 백그라운드에서 독립적으로 실행하도록 분리합니다.

1.2. std::mutex와 std::lock_guard

std::mutex는 스레드 간 동기화를 위해 사용되는 객체입니다. 여러 스레드가 동시에 하나의 자원에 접근할 경우 충돌이 발생할 수 있는데, 이를 방지하기 위해 뮤텍스를 사용합니다. std::lock_guard는 뮤텍스를 RAII 방식으로 관리하며, 블록을 벗어나면 자동으로 잠금을 해제해줍니다.

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void printMessage(int id) {
    std::lock_guard<std::mutex> guard(mtx);
    std::cout << "Thread " << id << " is printing." << std::endl;
}

int main() {
    std::thread t1(printMessage, 1);
    std::thread t2(printMessage, 2);
    t1.join();
    t2.join();
    return 0;
}

위 예제에서는 두 개의 스레드가 같은 함수 printMessage를 호출하지만, std::mutex를 통해 동기화되어 콘솔 출력이 충돌하지 않습니다.

📌 참고: 뮤텍스를 수동으로 lock/unlock할 수도 있으나, 예외 발생 시 unlock을 놓칠 수 있으므로 lock_guard 사용이 권장됩니다.

2. 병렬 알고리즘 (C++17 std::execution)

C++17에서는 <execution> 헤더를 통해 표준 라이브러리 알고리즘에 병렬 실행 정책을 적용할 수 있게 되었습니다. 이는 기존의 순차적 알고리즘(std::for_each, std::sort 등)에 병렬 처리를 적용하여 더 빠른 결과를 기대할 수 있습니다.

#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    std::for_each(std::execution::par, vec.begin(), vec.end(), [](int& n) {
        n *= 2;
    });

    for (int n : vec) {
        std::cout << n << " ";
    }

    return 0;
}

위 코드에서 std::execution::par은 병렬 실행 정책을 의미하며, vec의 모든 요소를 병렬로 처리하여 성능을 높입니다. 단, 병렬 처리 시에는 공유 자원 접근에 주의해야 하며, 알고리즘 내부에 상태 변화가 없는 경우에만 사용하는 것이 안전합니다.

⚠️ 주의: 병렬 알고리즘은 내부적으로 스레드를 사용하므로, 병렬화가 항상 성능 향상을 보장하지는 않습니다. 데이터 양이 작거나 캐시 성능이 중요한 경우에는 오히려 느려질 수 있습니다.

3. 원자 연산 (std::atomic)

std::atomic은 멀티스레딩 환경에서 동기화 없이 안전하게 정수나 포인터와 같은 간단한 자료형을 사용할 수 있도록 합니다. 내부적으로는 원자적(atomic) 연산을 보장하여, 여러 스레드가 동시에 값을 읽거나 쓸 때 경합 상태 없이 안전하게 동작합니다.

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> counter(0);

void incrementCounter() {
    for (int i = 0; i < 1000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);

    t1.join();
    t2.join();

    std::cout << "Final counter value: " << counter.load() << std::endl;
    return 0;
}

위 예제는 counterstd::atomic으로 선언하고, 두 스레드가 동시에 증가시켜도 정확히 2000이 출력됩니다. 이는 fetch_add() 연산이 원자적으로 동작하기 때문입니다.

💡 팁: std::memory_order는 메모리 일관성을 제어하는데 사용됩니다. 일반적인 경우 memory_order_relaxed가 성능상 유리하지만, 복잡한 동기화에는 acquire/release를 고려해야 합니다.

결론적으로, C++는 std::threadmutex, atomic 등의 도구를 통해 강력한 멀티스레딩 프로그래밍을 지원합니다. 병렬 알고리즘은 복잡한 동기화 없이도 고성능 프로그램을 작성하는 데 매우 유용하며, 정확한 이해와 활용이 필요합니다.

728x90
반응형

'Programming > C++' 카테고리의 다른 글

C++ : 전문가 프로젝트  (65) 2025.05.26
C++ : 대규모 프로젝트 아키텍처  (45) 2025.05.25
C++ : 성능 최적화  (82) 2025.05.24
C++ : 고급 템플릿  (78) 2025.05.22
C++ : 현대적 C++  (76) 2025.05.21
C++ : 스마트 포인터  (94) 2025.05.20
C++ : 고급 객체지향 설계  (94) 2025.05.19
C++ : 중급 프로젝트  (10) 2025.05.18