C++ 비동기 콜백에 shared_ptr를 복사해 넘겼습니다. 객체 수명과 여러 스레드의 필드 수정이 모두 안전해지나요?
잠깐, 답변을 보기 전에
먼저 내 말로 설명해 볼까요?
답변과 점검 포인트 펼치기답변 접기
구두 답변
정의를 외우기보다, 원리와 조건을 연결해 설명해 보세요.
shared_ptr는 소유자 수를 관리해 마지막 소유자가 사라질 때 객체를 정리하도록 돕습니다. 하지만 객체 내부 데이터의 동시 접근까지 보호하지는 않습니다. 저는 참조 카운트의 안전성, 포인터 변수의 접근, 객체 내용의 접근을 구분하겠습니다.
수명 연장과 동시 접근
예를 들어 두 콜백이 서로 다른 shared_ptr 복사본으로 같은 객체를 소유하면 마지막 복사본이 사라질 때까지 객체 수명은 유지됩니다. 하지만 둘이 동시에 객체의 일반 정수 count를 증가시키면 데이터 레이스가 될 수 있습니다. 참조 수를 안전하게 갱신하는 것과 대상 객체의 필드를 안전하게 바꾸는 것은 다릅니다.
서로 다른 shared_ptr 인스턴스가 같은 객체를 소유하며 각자 복사·파괴되는 것과, 같은 shared_ptr 변수 하나를 여러 스레드가 동시에 바꾸는 것은 다릅니다. 후자는 적절한 동기화나 원자적 shared_ptr가 필요합니다. 객체의 필드를 함께 수정한다면 그 필드도 락이나 소유권 규칙으로 보호해야 합니다. shared_ptr를 복사해 콜백에 넣으면 콜백 동안의 수명은 유지되지만, 그 객체가 종료·취소되지 않아 해당 작업을 계속해도 되는지는 별도로 확인해야 합니다.
공유 소유의 종료 규칙
저는 소유권이 한곳이면 unique_ptr를 우선하고, 실제로 여러 비동기 작업이 수명을 공유해야 할 때 shared_ptr를 쓰겠습니다. 서로를 강하게 참조하면 순환 때문에 해제되지 않을 수 있어 관찰자 관계에는 weak_ptr를 사용할 수 있습니다. 마지막 참조가 어느 스레드에서 사라지는지도 중요합니다. 무거운 소멸자가 이벤트 루프에서 실행되면 지연이 생길 수 있으므로, 수명 추적과 해제 시점의 지연을 함께 확인하겠습니다.
선택 기준과 검증
수명 연장이 오히려 종료 경합을 숨기는 반례가 있습니다. 콜백 큐가 포인터를 보유해 객체는 살아 있어도 세션은 이미 취소됐을 수 있으므로 결과를 적용하면 안 됩니다. 상태를 단일 executor에서 순차 처리하거나 불변 스냅샷으로 교체하고, weak_ptr::lock() 성공 뒤에도 세대와 취소 상태를 확인하겠습니다.
같은 원시 포인터에서 shared_ptr 두 개를 각각 새로 만들면 별개의 제어 블록이 생겨 이중 삭제로 이어질 수 있습니다. 이미 소유된 객체의 this를 공유해야 할 때도 새 shared_ptr(this)를 만들지 않고 적절한 enable_shared_from_this 계약을 사용해야 합니다. 생성자 안에서 아직 공유 소유권이 준비되지 않았는데 shared_from_this를 호출하는 것도 안전한 일반 패턴이 아닙니다.
make_shared는 객체와 제어 블록을 함께 할당할 수 있어 할당 횟수를 줄이지만, 객체가 파괴된 뒤에도 weak_ptr가 제어 블록을 유지하면 함께 할당한 저장 공간의 반환이 늦어질 수 있습니다. 객체 소멸과 메모리 블록 반환도 같은 시점이라고 단정하지 않겠습니다. 별칭 생성자로 만든 shared_ptr는 저장 포인터와 소유 객체가 다를 수 있으므로 get의 주소만으로 수명 소유자를 추측하지 않습니다. 이런 예외까지 필요한 경계에서만 공유 소유권을 사용하겠습니다.
득점 포인트
이해가 드러나는 지점을 확인해 보세요.
- 제어 블록·포인터 변수·대상 필드의 안전성을 구분한다.
- shared·unique·weak 소유 관계를 실제 수명에 맞춘다.
- 논리적 취소와 마지막 참조의 소멸 위치까지 검증한다.
감점 포인트
놓치기 쉬운 조건과 오해를 짚어 보세요.
- 참조 카운트 원자성이 대상 필드의 데이터 레이스도 막는다고 말한다.
- 같은 shared_ptr 변수의 동시 교체와 서로 다른 복사본을 혼동한다.
- shared_ptr가 객체의 논리적 취소나 순환 소유를 자동 해결한다고 단정한다.
더 파고들 거리
하나 더 질문하며 이해의 범위를 넓혀 보세요.
- weak_ptr::lock은 성공했지만 작업은 이미 취소됐습니다. 객체 생존과 현재 행동의 유효성을 어떻게 구분하나요?
- atomic shared_ptr로 루트를 교체합니다. 독자가 일관된 여러 필드를 읽으려면 객체 내부에 어떤 조건이 필요한가요?
- 마지막 참조의 소멸을 특정 executor로 보내려면 소유권을 어떻게 배치할까요?
득점·감점 포인트는 학습용 자기 점검 기준이며, 특정 회사의 실제 채점표가 아닙니다.
원리부터 공부하고 싶다면
답변을 보충하는 개념·예제·슈도코드를 읽어 보세요.