서론: 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
(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에 대한 동시 복제는 데이터 포인터와 제어 블록 포인터에 대한 읽기 및 복제만 수행한 다음 참조 카운트를 증가시키며, 참조 카운트 증가는 원자적 작업입니다. 따라서 이는 스레드 안전합니다.