C++ shared_ptr의 스레드 안전성 분석

서론: C++ 면접에서 종종 다음과 같은 질문을 접할 수 있습니다. shared_ptr는 스레드 안전한가요? 이 질문에 대한 답변을 위해 세 가지 동시성 시나리오를 고려해야 합니다: shared_ptr 복사의 안전성, shared_ptr에 대한 할당의 안전성, 그리고 shared_ptr가 가리키는 메모리 영역의 읽기/쓰기 안전성입니다.

먼저 다음과 같은 결론을 제시합니다:

  • 여러 스레드가 동시에 동일한 shared_ptr 객체를 복사하는 것은 문제가 없습니다. 왜냐하면 shared_ptr의 참조 카운트는 스레드 안전하기 때문입니다.
  • 여러 스레드가 동시에 동일한 shared_ptr 객체를 수정하는 것은 스레드 안전하지 않습니다.
  • 여러 스레드가 동시에 shared_ptr가 가리키는 메모리 객체를 읽고 쓰는 것은 스레드 안전하지 않습니다.

1. 참조 카운트 업데이트, 스레드 안전

여기서 우리는 shared_ptr를 복사하는 경우를 논의합니다. 이 작업은 참조 카운트를 읽고 쓰는 것이며, 참조 카운트의 업데이트는 원자적(atomic) 작업이므로 이 경우는 스레드 안전합니다. 아래 예제에서 두 스레드가 동시에 동일한 shared_ptr를 복사할 때, 참조 카운트 값은 항상 20001입니다.


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

constexpr int ITERATIONS = 10000;

int main() {
    std::shared_ptr<int> ptr = std::make_shared<int>(0);
    std::vector vec1(ITERATIONS);
    std::vector vec2(ITERATIONS);

    auto copy_reference = [&ptr](std::vector& vec) {
        for (int i = 0; i < ITERATIONS; i++) {
            vec[i] = ptr;
        }
    };

    std::thread t1(copy_reference, std::ref(vec1));
    std::thread t2(copy_reference, std::ref(vec2));
    t1.join();
    t2.join();
    
    std::cout << ptr.use_count() << std::endl; // 항상 20001
    
    return 0;
}

2. 메모리 영역 동시 읽기/쓰기, 스레드 불안전

아래 예제에서 두 스레드가 동시에 동일한 shared_ptr가 가리키는 메모리의 값을 증가시키는 작업을 수행할 때, 최종 결과는 우리가 기대한 20000이 아닙니다. 따라서 shared_ptr가 가리키는 메모리 영역을 동시에 수정하는 것은 스레드 안전하지 않습니다.


#include <memory>
#include <thread>
#include <iostream>

constexpr int NUM_INCREMENTS = 10000;

void increment_value(std::shared_ptr<int>& shared_num) {
    for (int i = 0; i < NUM_INCREMENTS; i++) {
        (*shared_num)++;
    }
}

int main() {
    std::shared_ptr<int> number = std::make_shared<int>(0);
    
    std::thread worker1(increment_value, std::ref(number));
    std::thread worker2(increment_value, std::ref(number));
    
    worker1.join();
    worker2.join();
    
    std::cout << "최종 number 값: " << *number << std::endl; // 가능한 결과: 16171, 20000이 아님
    
    return 0;
}

3. shared_ptr 객체 자체의 가리키는 대상 직접 수정, 스레드 불안전

아래 프로그램 예제에서 두 스레드가 동시에 동일한 shared_ptr 객체의 가리키는 대상을 수정할 때, 프로그램이 비정상적으로 종료됩니다.


#include <memory>
#include <thread>
#include <vector>

constexpr int MODIFICATIONS = 1000000;
constexpr int THREAD_COUNT = 10;

int main() {
    std::shared_ptr<int> sptr = std::make_shared<int>(1);
    
    auto modify_pointer = [&sptr]() {
        for (int i = 0; i < MODIFICATIONS; ++i) {
            sptr = std::make_shared<int>(i);
        }
    };
    
    std::vector threads;
    for (int i = 0; i < THREAD_COUNT; ++i) {
        threads.emplace_back(modify_pointer);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

다음과 같은 오류가 발생합니다:


pure virtual method called
terminate called without an active exception

gdb로 함수 호출 스택을 확인하면 `std::shared_ptr::~shared_ptr()` 호출 시 오류가 발생한 것을 볼 수 있습니다:


(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007ffff7bc7859 in __GI_abort () at abort.c:79
#2  0x00007ffff7e73911 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007ffff7e7f38c in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007ffff7e7f3f7 in std::terminate() () from /lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007ffff7e80155 in __cxa_pure_virtual () from /lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x00005555555576c2 in std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() ()
#7  0x00005555555572fd in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() ()
#8  0x0000555555557136 in std::__shared_ptr::~__shared_ptr() ()
#9  0x000055555555781c in std::__shared_ptr::operator=(std::__shared_ptr&&) ()
#10 0x00005555555573d0 in std::shared_ptr<int>::operator=(std::shared_ptr<int>&&) ()
#11 0x000055555555639f in main::{lambda()#1}::operator()() const ()
...

원인은 동시 수정 시, 현재 소멸 중인 객체에 대해 다시 소멸자를 호출하여 정의되지 않은 동작이 발생했기 때문입니다.

프로그램에 잠금을 추가하면 프로그램이 정상적으로 실행됩니다:


#include <memory>
#include <thread>
#include <vector>
#include <mutex>

constexpr int MODIFICATIONS = 1000000;
constexpr int THREAD_COUNT = 10;

int main() {
    std::shared_ptr<int> sptr = std::make_shared<int>(1);
    std::mutex mtx;
    
    auto safe_modify = [&sptr, &mtx]() {
        std::lock_guard lock(mtx);
        for (int i = 0; i < MODIFICATIONS; ++i) {
            sptr = std::make_shared<int>(i);
        }
    };
    
    std::vector threads;
    for (int i = 0; i < THREAD_COUNT; ++i) {
        threads.emplace_back(safe_modify);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << *sptr << std::endl;  // 예상대로 실행됨, 결과: 999999
    
    return 0;
}

요약

  • shared_ptr는 가리키는 객체 메모리 영역에 대한 스레드 안전 보호를 제공하지 않으므로, 해당 메모리 영역에 대한 동시 읽기/쓰기는 안전하지 않습니다.
  • 할당 작업은 원래 메모리 해제, 포인터 가리키는 대상 수정 등 여러 수정 작업을 포함하므로 이 과정은 원자적 작업이 아니며, 따라서 shared_ptr에 대한 동시 할당은 스레드 안전하지 않습니다.
  • shared_ptr에 대한 동시 복제는 데이터 포인터와 제어 블록 포인터에 대한 읽기 및 복제만 수행한 다음 참조 카운트를 증가시키며, 참조 카운트 증가는 원자적 작업입니다. 따라서 이는 스레드 안전합니다.

태그: C++ shared_ptr 스레드 안전성 메모리 관리 동시성

8월 14일 07:03에 게시됨