프로그래밍/C++

C++ 멀티스레딩: 스레드 생성, 동기화, 병렬 프로그래밍 활용하기

shimdh 2025. 2. 2. 21:10
728x90

1. 스레드란 무엇인가?

스레드는 프로세스 내에서 실행되는 독립적인 실행 경로를 의미합니다. 여러 스레드가 하나의 프로세스 내에서 동시에 실행되며, 다음과 같은 특징을 가집니다:

  • 메모리 공유: 동일한 프로세스의 여러 스레드는 같은 메모리 공간을 사용합니다.
  • 경량성: 프로세스보다 생성과 종료가 더 빠르고 효율적입니다.
  • 병렬성: 멀티코어 프로세서를 활용하여 물리적으로 동시에 실행이 가능합니다.

스레드는 멀티태스킹의 기반이 되며, 복잡한 응용 프로그램에서 성능을 향상시키기 위해 자주 사용됩니다. 예를 들어, 게임 엔진에서는 물리 계산과 렌더링 작업을 병렬로 수행하여 보다 빠르고 효율적인 처리가 가능합니다. 특히 게임 엔진, 웹 서버, 실시간 데이터 처리 시스템 등에서 스레드 활용은 필수적입니다.

멀티스레딩을 활용하면 대규모 데이터 처리, 동시성 높은 시스템 구현, 사용자 인터페이스와 백엔드 작업의 분리를 효율적으로 처리할 수 있습니다. 스레드는 또한 IO 작업이나 대기 시간이 긴 작업에서도 효율적인 성능을 보장합니다.


2. C++에서 스레드 생성하기

C++11부터는 <thread> 헤더를 사용하여 쉽게 스레드를 생성할 수 있습니다. 스레드 생성은 독립적인 실행 경로를 추가하는 과정으로, 메인 함수와 동시에 실행될 수 있습니다. 다음은 간단한 예제입니다:

#include <iostream>
#include <thread>

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

int main() {
    std::thread t(printMessage); // 새로운 스레드 생성

    std::cout << "Hello from main!" << std::endl;

    t.join(); // 생성된 스레드가 종료될 때까지 대기
    return 0;
}

이 코드는 메인 스레드와 새로운 스레드에서 각각 메시지를 출력합니다. t.join()을 통해 메인 스레드는 새로 생성된 스레드가 완료될 때까지 기다립니다. 스레드의 생명 주기를 관리하는 것은 멀티스레딩 프로그래밍에서 중요한 부분입니다.

스레드의 장점과 단점

장점

  1. 비동기 처리: 스레드는 IO 작업을 병렬로 처리하여 응답성을 높입니다.
  2. 병렬 계산: 멀티코어 환경에서 계산 작업을 분할하여 성능을 극대화합니다.
  3. 자원 공유: 메모리와 같은 자원을 효율적으로 사용할 수 있습니다.

단점

  1. 복잡성 증가: 코드가 복잡해지고 디버깅이 어려워질 수 있습니다.
  2. 동기화 비용: 데이터 경합 문제를 해결하기 위해 동기화가 필요하며, 이는 성능 저하를 유발할 수 있습니다.
  3. 자원 소모: 스레드가 과도하게 생성되면 메모리와 CPU 자원을 낭비할 수 있습니다.

하지만 스레드는 복잡성과 디버깅의 어려움을 동반합니다. 적절한 동기화와 에러 처리가 필수적입니다.


3. 여러 개의 스레드 생성

멀티스레딩의 장점 중 하나는 여러 작업을 병렬로 처리할 수 있다는 점입니다. C++에서는 다음과 같이 여러 개의 스레드를 생성하여 병렬 처리를 수행할 수 있습니다:

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

void printNumber(int n) {
    std::cout << "Number: " << n << std::endl;
}

int main() {
    const int numThreads = 5;
    std::vector<std::thread> threads;

    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(printNumber, i); // 각각 다른 숫자를 출력하는 스레드 생성
    }

    for (auto& t : threads) {
        t.join(); // 모든 스레드가 종료될 때까지 대기
    }

    return 0;
}

이 코드는 총 5개의 스레드를 생성하여 각각 다른 숫자를 출력합니다. 병렬 처리를 통해 프로그램의 작업 속도를 높일 수 있습니다.

다중 스레드 생성 시 유의점

여러 스레드를 생성할 때는 다음 사항을 고려해야 합니다:

  1. 하드웨어 제한: 생성한 스레드 수가 CPU 코어 수를 초과하면, 오히려 성능이 저하될 수 있습니다.
  2. 자원 관리: 과도한 스레드 사용은 메모리 부족을 초래할 수 있습니다.
  3. 데이터 경합: 동시 접근이 필요한 자원에 대한 보호가 필요합니다.

4. 자원 관리와 동기화

동기화의 필요성

멀티스레드 환경에서는 여러 스레드가 동시에 자원에 접근하면서 데이터 경합(Data Race) 문제가 발생할 수 있습니다. 데이터 경합은 프로그램의 예상치 못한 동작을 초래할 수 있습니다. 이를 방지하기 위해 동기화 기법이 필요합니다.

4.1 뮤텍스를 사용한 동기화

뮤텍스는 "Mutual Exclusion"의 약자로, 한 번에 하나의 스레드만 특정 코드 블록에 접근하도록 보장합니다:

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

std::mutex mtx;

void safePrint(const std::string& message) {
    mtx.lock(); // 보호 구역 시작
    std::cout << message << std::endl;
    mtx.unlock(); // 보호 구역 종료
}

int main() {
    std::thread t1(safePrint, "Thread A");
    std::thread t2(safePrint, "Thread B");

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

    return 0;
}

이 코드는 두 스레드가 동시에 콘솔에 접근하지 않도록 뮤텍스를 사용해 보호합니다. 그러나 lockunlock의 명시적인 호출은 코드 가독성을 떨어뜨릴 수 있으므로, std::lock_guard를 사용하는 것이 좋습니다:

void safePrintWithGuard(const std::string& message) {
    std::lock_guard<std::mutex> guard(mtx);
    std::cout << message << std::endl;
}

4.2 조건 변수

조건 변수는 특정 조건이 충족될 때까지 스레드를 대기하게 하거나 깨우는 데 사용됩니다. 대표적인 사용 사례로는 생산자-소비자 패턴이 있습니다. 예를 들어, 한 스레드(생산자)가 데이터를 생성하고 다른 스레드(소비자)가 이를 처리하는 시스템에서, 조건 변수를 이용해 소비자가 데이터가 준비될 때까지 대기하도록 설정할 수 있습니다. 이를 통해 효율적인 작업 흐름을 유지할 수 있습니다. 이는 생산자-소비자 문제와 같은 상황에서 유용합니다:

#include <iostream>
#include <thread>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void waitTask() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; }); // ready가 true가 될 때까지 대기
    std::cout << "Task completed!" << std::endl;
}

void signalTask() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_all(); // 모든 대기 중인 스레드에 알림
}

int main() {
    std::thread t1(waitTask);
    std::thread t2(signalTask);

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

    return 0;
}

위 코드는 조건 변수를 사용하여 스레드 간 작업 순서를 제어합니다.

4.3 세마포어

C++20부터는 표준 라이브러리에 세마포어가 포함되었습니다. 세마포어는 리소스에 접근할 수 있는 스레드의 수를 제한하는 데 사용됩니다.

#include <iostream>
#include <thread>
#include <semaphore>

std::counting_semaphore<3> sem(3); // 최대 3개의 스레드 허용

void limitedAccess(int id) {
    sem.acquire(); // 접근 허용
    std::cout << "Thread " << id << " is accessing the resource." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    sem.release(); // 접근 해제
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(limitedAccess, i);
    }
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}

이 코드는 최대 3개의 스레드만 동시에 자원에 접근하도록 제한합니다.


5. 병렬 프로그래밍

병렬 프로그래밍은 여러 작업을 동시에 처리하여 성능을 극대화하는 기법입니다. 이는 대규모 데이터 처리나 복잡한 계산에 유용합니다.

예제: 벡터 요소 병렬 합산

#include <iostream>
#include <vector>
#include <numeric>
#include <thread>

void sumPart(const std::vector<int>& vec, int start, int end, int& result) {
    result = std::accumulate(vec.begin() + start, vec.begin() + end, 0);
}

int main() {
    const int size = 1000000;
    std::vector<int> vec(size, 1); // 모든 요소를 1로 초기화

    int result1 = 0, result2 = 0;
    std::thread t1(sumPart, std::cref(vec), 0, size / 2, std::ref(result1));
    std::thread t2(sumPart, std::cref(vec), size / 2, size, std::ref(result2));

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

    std::cout << "Total Sum: " << (result1 + result2) << std::endl;
    return 0;
}

위 코드는 큰 벡터를 두 부분으로 나누어 각각 다른 스레드에서 합산 작업을 수행합니다. 이를 통해 처리 시간을 단축할 수 있습니다.

병렬 프로그래밍 패턴

  1. 데이터 분할(Data Partitioning): 큰 데이터를 여러 부분으로 나누어 병렬 처리.
  2. 파이프라인(Pipeline): 각 단계가 병렬로 실행되도록 구성.
  3. 작업 풀(Task Pool): 작업을 동적으로 할당하여 효율적으로 처리.

병렬 프로그래밍을 사용할 때는 항상 작업 간 의존성을 주의해야 하며, 적절한 동기화와 병렬화 전략이 필요합니다. 작업 간 의존성이 높은 경우, 성능이 저하될 수 있으므로 이를 최소화해야 합니다. 예를 들어, 작업을 병렬로 나누기 전에 데이터 흐름과 의존성을 철저히 분석하여 독립적인 작업 단위로 분할하는 것이 중요합니다. 또한, 과도한 동기화는 성능 병목을 초래할 수 있으므로 꼭 필요한 부분에만 적용해야 합니다.


결론

C++의 멀티스레딩은 성능 최적화와 응답성 향상을 위한 강력한 도구입니다. 스레드 생성과 관리, 동기화 기법, 병렬 프로그래밍 패턴을 적절히 활용하면 다양한 문제를 효율적으로 해결할 수 있습니다. 이러한 기술은 특히 게임 개발, 데이터 처리, 실시간 시스템과 같은 분야에서 큰 가치를 발휘합니다.

멀티스레딩을 활용한 코드 작성을 통해 더 나은 프로그램을 만들어보세요!

멀티스레딩 프로그래밍은 학습과 실무 모두에서 중요한 역량이므로, 다양한 시나리오에서 이를 실험하고 개선하는 노력이 필요합니다.

728x90