C++ STL: 7장 - STL과 멀티스레딩 활용

2025. 2. 26. 18:16프로그래밍 언어/C++ STL

7.1 C++과 멀티스레딩 개요

C++11부터 표준 라이브러리에서 멀티스레딩 지원이 강화되었습니다. STL은 멀티스레딩을 직접적으로 지원하지는 않지만, C++ 표준 스레딩 라이브러리(<thread>, <mutex>, <condition_variable> 등)와 함께 사용하여 멀티스레딩 환경에서 STL 컨테이너와 알고리즘을 효과적으로 활용할 수 있습니다.


7.2 STL 컨테이너와 멀티스레딩

7.2.1 컨테이너의 스레드 안전성

  • STL 컨테이너는 기본적으로 스레드 안전하지 않습니다.
    • 단일 스레드에서 사용하거나, 여러 스레드가 컨테이너에 읽기만 수행할 때는 안전합니다.
    • 쓰기 작업이 포함된 경우, 동기화(std::mutex)가 필요합니다.

7.2.2 컨테이너 동기화 기법

  • 뮤텍스 사용: 스레드 간 동기화를 위해 std::mutex를 사용하여 데이터 접근을 제어합니다.

예제:

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

std::vector<int> data;
std::mutex data_mutex;

void add_data(int value) {
    std::lock_guard<std::mutex> lock(data_mutex);
    data.push_back(value);
}

int main() {
    std::thread t1(add_data, 1);
    std::thread t2(add_data, 2);

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

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

    return 0;
}

7.2.3 멀티스레딩에 적합한 컨테이너 선택

  • 읽기 중심 작업:
    • 추천: std::vector, std::deque, std::array (읽기 작업은 스레드 안전).
  • 동시 쓰기 작업:
    • 동기화를 직접 구현하거나, 멀티스레딩을 지원하는 컨테이너(예: tbb::concurrent_vector)를 사용하는 것이 좋습니다.

Note: Intel TBB와 Boost 라이브러리는 멀티스레드 지원을 강화하지만, STL의 일부가 아닙니다.


7.3 STL 알고리즘과 멀티스레딩

7.3.1 병렬 알고리즘

C++17에서는 <execution> 헤더를 통해 STL 알고리즘에 병렬 실행 정책이 도입되었습니다. 이를 사용하면 STL 알고리즘을 멀티스레드로 병렬화할 수 있습니다.

  • 실행 정책:
    • std::execution::seq: 순차 실행 (기본값).
    • std::execution::par: 병렬 실행.
    • std::execution::par_unseq: 병렬 + 비순차 실행 (가장 빠름).

예제:

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

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

    std::sort(std::execution::par, vec.begin(), vec.end());

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

    return 0;
}

7.3.2 병렬 알고리즘 사용 시 주의사항

  1. 데이터 크기: 병렬화를 사용하는 경우, 데이터 크기가 충분히 크지 않으면 오히려 성능이 저하될 수 있습니다.
  2. 데이터 동기화: 공유 자원에 접근하는 경우 동기화가 필요합니다.
  3. 알고리즘 지원 여부: 모든 STL 알고리즘이 병렬 실행을 지원하지는 않습니다.

7.4 락 없는 프로그래밍

7.4.1 락 없는 자료구조

  • 멀티스레딩 환경에서 락을 사용하지 않고 데이터를 안전하게 처리하기 위한 기법입니다.
  • C++ 표준에는 락 없는 컨테이너가 포함되어 있지 않으나, 외부 라이브러리(예: Intel TBB, Boost)에서 지원합니다.

7.4.2 원자적 연산

  • std::atomic을 사용하면 락 없이 스레드 간 데이터를 안전하게 읽고 쓸 수 있습니다.

예제:

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

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

void increment() {
    for (int i = 0; i < 1000; ++i) {
        ++counter;
    }
}

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

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

    std::cout << "Counter: " << counter << "\n";
    return 0;
}

7.5 멀티스레딩 활용 사례

7.5.1 데이터 처리 병렬화

  • 상황: 대규모 데이터 정렬 또는 변환.
  • 해결책: std::sort와 같은 알고리즘에 std::execution::par를 사용하여 병렬화.

7.5.2 스레드 풀 구현

  • 상황: 여러 작업을 효율적으로 관리.
  • 해결책: 사용자 정의 스레드 풀 구현.

예제:

#include <iostream>
#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>

class ThreadPool {
public:
    ThreadPool(size_t threads);
    ~ThreadPool();

    template <class F>
    void enqueue(F&& f);

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;

    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

ThreadPool::ThreadPool(size_t threads) : stop(false) {
    for (size_t i = 0; i < threads; ++i) {
        workers.emplace_back([this] {
            while (true) {
                std::function<void()> task;
                {
                    std::unique_lock<std::mutex> lock(this->queue_mutex);
                    this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); });
                    if (this->stop && this->tasks.empty())
                        return;
                    task = std::move(this->tasks.front());
                    this->tasks.pop();
                }
                task();
            }
        });
    }
}

ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread& worker : workers)
        worker.join();
}

template <class F>
void ThreadPool::enqueue(F&& f) {
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        tasks.emplace(std::forward<F>(f));
    }
    condition.notify_one();
}

int main() {
    ThreadPool pool(4);

    for (int i = 0; i < 8; ++i) {
        pool.enqueue([i] {
            std::cout << "Task " << i << " is running.\n";
        });
    }

    return 0;
}

Note: 위 예제는 간단한 스레드 풀 구현을 보여줍니다. 실제 사용 시에는 예외 처리, 작업 우선순위, 최적화 등이 필요할 수 있습니다.


결론

STL과 C++ 멀티스레딩 기능을 결합하면 강력한 병렬 프로그래밍이 가능합니다. STL 컨테이너와 알고리즘을 적절히 동기화하고, 병렬 알고리즘과 원자적 연산을 활용하면 안전하고 효율적인 멀티스레드 애플리케이션을 작성할 수 있습니다.