병렬 프로그래밍

스레드 안전(Thread Safety) 문제와 이를 해결하는 방법

ROBL 2025. 2. 4.
728x90
반응형

스레드 안전(Thread Safety)은 멀티스레딩 환경에서 여러 스레드가 동시에 자원에 접근할 때, 데이터 경쟁(Data Race)이나 상태 불일치를 방지하는 중요한 개념입니다. 멀티스레드 환경에서는 여러 스레드가 동시에 동일한 자원에 접근하고 수정하려고 할 수 있기 때문에, 이를 제어하지 않으면 예기치 못한 동작을 일으킬 수 있습니다. 스레드 안전을 보장하는 것은 고성능 멀티스레드 프로그램을 개발하는 데 필수적인 기술입니다.

이 포스팅에서는 스레드 안전의 개념을 소개하고, 이를 해결하기 위한 기법들을 C++ 예제 코드와 함께 설명하겠습니다.

1. 스레드 안전이란?

스레드 안전은 여러 스레드가 동시에 실행되는 상황에서, 공유된 자원에 대한 접근이 제대로 제어되어 예기치 않은 결과를 초래하지 않는 상태를 의미합니다. 스레드 안전을 보장하기 위해서는 자원에 대한 접근을 동기화(Synchronization)하여 한 번에 하나의 스레드만 자원을 수정할 수 있도록 해야 합니다.

스레드 안전을 보장하지 않는 코드에서는 여러 스레드가 동시에 자원을 수정하려 할 때, 경쟁 상태(Race Condition)가 발생할 수 있습니다. 경쟁 상태는 두 개 이상의 스레드가 동시에 자원에 접근하여 데이터를 변경하거나 읽기할 때 발생하는 문제입니다. 이로 인해 프로그램이 예기치 않게 동작하거나 데이터가 손상될 수 있습니다.

2. 스레드 안전 문제의 예시

다음은 C++로 작성된 스레드 안전하지 않은 예시입니다. 두 스레드가 동일한 전역 변수에 접근하여 값을 증가시키는 코드입니다.

#include <iostream>
#include <thread>

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 << std::endl; // 예상값 2000
    return 0;
}

위 코드는 두 스레드가 동시에 counter 변수의 값을 증가시키는 작업을 합니다. 그러나 이 코드에는 스레드 안전을 보장하지 않기 때문에, 두 스레드가 counter 값을 동시에 수정할 수 있습니다. 이로 인해 경쟁 상태가 발생하고, counter의 최종 값이 2000이 아니라 그보다 작은 값이 출력될 가능성이 높습니다.

3. 스레드 안전 문제 해결 방법

스레드 안전 문제를 해결하는 방법은 여러 가지가 있으며, 주로 동기화 기법을 사용합니다. C++에서는 동기화를 위한 다양한 기법을 제공하고 있습니다.

3.1. 뮤텍스(Mutex) 사용

뮤텍스는 한 번에 하나의 스레드만 자원에 접근하도록 잠금을 사용하는 기법입니다. std::mutex를 사용하여 자원을 보호할 수 있습니다.

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

int counter = 0; // 전역 변수
std::mutex mtx;  // 뮤텍스

void increment() {
    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 뮤텍스를 잠금
        ++counter; // 안전한 자원 접근
    }
}

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

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

    std::cout << "Counter: " << counter << std::endl; // 예상값 2000
    return 0;
}

위 코드에서는 std::lock_guard를 사용하여 counter를 수정할 때마다 뮤텍스를 잠그고 해제합니다. 이를 통해 두 스레드가 동시에 counter에 접근하지 않도록 보장하며, 경쟁 상태를 해결할 수 있습니다.

3.2. 원자적 연산(Atomic Operations)

원자적 연산은 자원에 대한 접근을 중단 없이 한 번에 처리하는 방법입니다. std::atomic을 사용하여 변수에 대한 원자적 작업을 수행할 수 있습니다. 원자적 연산은 뮤텍스보다 더 효율적인 방법이 될 수 있으며, 특히 자주 접근하는 공유 데이터에 유용합니다.

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

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.load() << std::endl; // 예상값 2000
    return 0;
}

std::atomic을 사용하면 counter에 대한 모든 접근이 원자적으로 이루어지며, 경쟁 상태를 방지할 수 있습니다. 원자적 연산은 동기화 오버헤드를 줄일 수 있어 성능이 중요한 경우에 유리합니다.

3.3. 스레드 안전한 자료구조 사용

C++ 표준 라이브러리에는 스레드 안전을 보장하는 자료구조가 제한적이지만, 외부 라이브러리나 C++17부터 제공되는 스레드 안전한 자료구조를 사용할 수 있습니다. 예를 들어, std::shared_mutex를 이용한 읽기-쓰기 자원 관리나, 등의 자료구조에서 멀티스레드 접근을 안전하게 처리할 수 있는 방법들이 있습니다.

4. 결론

스레드 안전(Thread Safety)은 멀티스레드 프로그램에서 발생할 수 있는 경쟁 상태나 데이터 손상을 방지하는 핵심 요소입니다. 이를 해결하기 위해서는 뮤텍스, 원자적 연산, 쓰레드 안전한 자료구조 등의 기법을 적절히 사용해야 합니다. 멀티스레딩 환경에서 자원을 공유하는 경우, 반드시 동기화를 통해 스레드 안전을 보장해야 하며, 프로그램의 성능과 안정성을 크게 향상시킬 수 있습니다.

다음 포스팅에서는 스레드 동기화의 성능 최적화에 대해 다루겠습니다.

728x90
반응형

댓글

💲 추천 글