본문 바로가기
Language/C++

std::weak_ptr를 통한 shared_ptr 생성은 멀티스레드 환경에서 안전한가?

by W00gie 2026. 1. 24.

목표

멀티스레드 환경에서 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 만을 가진다.