목표
멀티스레드 환경에서 weak_ptr의 lock()을 통한 shared_ptr 생성의 결과가
유효한 shared_ptr, nullptr 이외에 half-alive 상태일 가능성에 대해 포스팅한다.
1. weak_ptr에 관하여
1.1 정의 및 사용목적
std::weak_ptr은 소유하지 않는 shared_ptr 이다.
shared_ptr로 관리되는 객체에 대해 참조만 할 뿐, use_count를 증가시키지 않는다.
이를 통해서 궁극적으로 weak_ptr은 shared_ptr의 순환참조를 예방하는 역할을 수행할 수 있다.
본격적인 예시코드를 읽기전에 shared_ptr로 관리되는 객체는 오직 use_count가 0으로 되었을 때 소멸될 수 있음을 상기한다.
struct B;
struct A {
std::shared_ptr<B> b;
};
struct B
{
std::shared_ptr<A> get_A()
{
return a_weak.lock();
}
std::weak_ptr<A> a_weak; // 🔥 여기
};
위와 같이 A,B 두 객체 사이에 상호참조하는 과정에서 B에서는 A의 shared_ptr이 아닌 weak_ptr을 통해 참조한다.
weak_ptr이 아닌 양방 shared_ptr로 참조할경우, dead lock과 동일하게 서로간에 리소스를 해제할 수 없는 상황에 놓이게 되기 때문이다. A의 원본 객체에 대해서 액세스 할때는 a_weak.lock()을 통해 std::shared_ptr의 유효성을 검증한 후 복사해오게 된다.
1.2 weak_ptr와 제어블록의 생명주기
weak_ptr 의 생명주기에 대해 알기위해서는 제어 블록(controler block)에 대해 우선적으로 알아야한다.
제어블록은 shared_ptr 가 생성과정에서 생성되어, 아래와 같은 정보들을 저장한다.
- use_count (shared_count)
- weak_count
- 관리 객체 포인터
- 삭제자(deleter)
- allocator 정보
제어블록의 경우 여기서 shared_count 와 weak_count가 0일때 소멸된다.
다만 use_count는 weak_ptr가 살아있을경우 카운트가 유지되므로
따라서 객체가 소멸되어도 제어블록은 살아있고, 제어블록의 생명을 연장하는것은 weak_ptr이라 할 수 있다.
weak_ptr의 소멸전에는 제어블록이 먼저 소멸될 수 없다는 점을 기억하고 다음으로 넘어간다.
2. weak_ptr을 통한 shared_ptr의 생성
2.1 생성과정
weak_ptr을 통한 생성과정은
1. shared_ptr의 유효성 검증
2. 원본 객체 복사
순서로 이어진다. 해당 과정을 수행할 수 있는 lock() 멤버함수로 대체한다.
https://en.cppreference.com/w/cpp/memory/weak_ptr.html
std::weak_ptr - cppreference.com
template< class T > class weak_ptr; (since C++11) std::weak_ptr is a smart pointer that holds a non-owning ("weak") reference to an object that is managed by std::shared_ptr. It must be converted to std::shared_ptr in order to access the referenced object.
en.cppreference.com
2.1 weak_ptr::lock() 세부 실행과정
libstdc++ 에 정의된 weak_ptr::lock 의 코드이다.
bool
_M_add_ref_lock_nothrow() noexcept
{
long __count = _M_use_count.load(std::memory_order_relaxed);
while (__count != 0) // 0이 되면 다시 1로 전환되지 못한다.
{
if (_M_use_count.compare_exchange_weak( // CAS로 원자성보장
__count, __count + 1,
std::memory_order_acquire,
std::memory_order_relaxed))
return true;
}
return false;
}
shared_ptr(const weak_ptr<_Tp>& __r, std::nothrow_t) noexcept
{
_M_ptr = nullptr;
_M_refcount = __r._M_refcount;
if (_M_refcount
&& _M_refcount->_M_add_ref_lock_nothrow())
_M_ptr = __r._M_ptr;
else
_M_refcount = nullptr;
}
위의 코드에서 중요한 부분은 아래와 같다.
1)_M_add_ref_lock_nothrow 함수에서 use_count를 CAS 방식으로 연산
2) CAS 연산 이후에 use_count의 0에서 1로 올리지 않는다.
추가로 1.2에서 명시하였듯이 제어 블록은 객체의 소멸보다, weak_ptr의 소멸보다 우선될 수 없다는 점 또한 확인하였다.
결론
- 원본객체, 제어 블록, weak_ptr의 생명주기에 따르면 weak_ptr 연산 도중 제어블록이 삭제 될 가능성이 없다.
- 내부 lock()의 구현은 CAS 및 루프를 통해 0에서 1로 객체 파괴 이후 use_count를 재증가 시키는 케이스를 차단하고 있다.
따라서 weak_ptr을 통한 lock()의 실행 결과는 멀티스레드 환경에서 객체의 참조 혹은 nullptr 만을 가진다.
'Language > C++' 카테고리의 다른 글
| Variant 초기화의 중요성 bad_variant_access (0) | 2022.06.03 |
|---|---|
| 상수 멤버함수의 설계 방법 (0) | 2022.03.02 |
| 참조 전달 문법 std::move, std::forward (0) | 2022.01.13 |
| 이동생성자와 보편참조법(universal ref) (0) | 2022.01.07 |
| enable_shared_from_this의 사용법 (0) | 2022.01.04 |