본문 바로가기

C++ 개발이야기

실무에서 경험한 C++ 멀티스레딩과 스마트 포인터 문제 및 해결 방법

SMALL

포스팅 계기

 C++에서 멀티스레딩을 구현할 때, 스마트 포인터를 활용하면 자원 관리가 편리해질 것 같지만, 오히려 더 복잡한 문제를 초래할 수도 있다. 특히, 직접 스마트 포인터를 구현해서 사용하면서 데드락(Deadlock), 레이스 컨디션(Race Condition), Dangling Reference, Reference Count 문제, 성능 이슈 등을 모두 경험하면서 많은 시행착오를 겪었다.

 이 글에서는 직접 스마트 포인터를 구현하면서 발생한 문제와 해결 방법을 공유하고, 실무에서 멀티스레딩을 안전하게 구현하는 팁을 정리해 보겠다.

 


직접 스마트 포인터를 구현한 이유

 일반적으로 std::shared_ptr이나 std::unique_ptr을 사용하면 안전한 메모리 관리를 할 수 있다. 하지만 특정한 환경에서는 커스텀 스마트 포인터가 필요할 때가 있다.

제가 직접 스마트 포인터를 구현한 이유는 다음과 같다:

  • 특정한 메모리 관리 전략을 적용하기 위해 (usedCount == 1일 때만 삭제)
  • 프로그램 종료 시 자동으로 해제되도록 설계하기 위해
  • 멀티스레드 환경에서 동작하도록 최적화하기 위해
  • 내가 필요한 기능만 직접 구현하여 가볍게 라이브러리화 하기 위해

이러한 목표를 가지고 스마트 포인터를 직접 설계했지만, 여러 가지 문제점에 직면했었다. 그 직면한 문제를 공유하고 어떻게 해결했는지 밑에 정리해 보겠다.


발생한 문제들

  • 레이스 컨디션 (Race Condition)
    • 스레드 간에 공유되는 스마트 포인터의 참조 카운트가 동시에 변경되면서 예기치 않은 동작이 발생했다
    • 여러 스레드가 동시에 참조 카운트를 변경하면서 객체가 중복 삭제되거나, 삭제되지 않는 문제가 발생했다.
if (refCount == 0) {
    delete ptr;  // 다른 스레드에서 동시에 접근하면 문제가 발생할 수 있음
}

 

  • 데드락 (Deadlock)
    • 스마트 포인터를 참조하는 중 한 스레드가 락을 걸고 다른 스레드가 같은 락을 기다리는 문제가 발생했다.
    • 특히, 스마트 포인터 내부에서 mutex를 걸어놓고, 다른 스레드에서 같은 객체를 접근하면 교착 상태가 발생했다.
  • Dangling Reference (소멸된 객체 참조)
    • shared_ptr을 구현할 때, 참조 카운트가 남아있다고 생각했지만, 실제 객체는 이미 삭제된 경우가 있었다..
    • 특정한 스레드에서 스마트 포인터를 사용 중인데, 다른 스레드에서 이미 삭제해 버려유효하지 않은 메모리를 참조하는 문제가 발생했다.
  • Reference Count 동기화 문제
    • 스마트 포인터의 참조 카운트를 관리하는 과정에서 증가/감소 연산이 원자적이지 않아 비정상적인 동작이 발생했다.
    • std::atomic<int>을 사용하지 않고 단순한 int 카운터를 사용하면서 동기화 문제 발생.
  • 성능 문제
    • mutex를 사용하면서 참조 카운트 증감 연산이 많아지면 **락 경합(Lock Contention)**이 발생하여 성능이 저하되었다.

해결 방법

  • Mutex를 활용한 Reference Count 관리
    • 참조 카운트를 변경할 때 mutex를 사용하여 안전하게 동기화
    • 카운트가 0이 되었을 때만 안전하게 객체 삭제
class CustomSharedPtr {
private:
    T* ptr;
    int refCount;
    std::mutex mtx;

public:
    void addRef() {
        std::lock_guard<std::mutex> lock(mtx);
        refCount++;
    }
    
    void release() {
        std::lock_guard<std::mutex> lock(mtx);
        refCount--;
        if (refCount == 0) {
            delete ptr;
        }
    }
};
    • 객체 소멸 시점 제어 (메모리 부족 방지)
      • 프로그램이 종료될 때 자동으로 객체가 해제되도록 하고, 메모리가 부족할 때만 즉시 삭제
if (usedCount == 1) {
    delete ptr;  // 관리하는 스레드만 접근할 때 삭제하여 메모리 확보
}

 

  • std::atomic을 활용하여 Reference Count 원자화
    • mutex 대신 std::atomic을 활용하여 락 없이 카운트 증가/감소 연산을 수행
    • 성능 개선 및 동기화 문제 해결
std::atomic<int> refCount;

 

  • weak_ptr 개념 도입하여 Dangling Reference 방지
    • weak_ptr 개념을 활용하여 객체가 삭제되었는지 확인한 후 접근
    • Dangling Reference 문제 해결
std::weak_ptr<T> weakRef = sharedPtr;
if (auto validRef = weakRef.lock()) {
    // 안전하게 사용 가능
}

실무에서 배운 교훈

🔥 1. 스마트 포인터를 잘못 구현하면 디버깅이 극도로 어려워진다.

  • 죽는 곳이 한 곳이 아니라 여러 곳이라 문제를 찾기가 어려웠다.
  • 스마트 포인터 내부에서 메모리 관리가 꼬이면 예상하지 못한 곳에서 크래시가 발생한다.

🔥 2. 같은 포인터를 공유하는 경우 상호배제(Mutex)가 필수

  • 멀티스레드 환경에서 동기화 없이 스마트 포인터를 사용하면 위험하다.
  • "Reference Count" 관리에 mutex 또는 std::atomic을 사용해야 한다.

🔥 3. 멀티스레드 구현 시 메모리 커럽션을 방지해야 한다.

  • 멀티스레드 시작 시점과 종료 시점을 명확하게 해야 한다.
  • 예외 상황을 고려하여 중단할 수 있도록 예외 처리를 신경 써야 한다.

마무리

 C++에서 스마트 포인터는 강력한 도구이지만, 멀티스레드 환경에서 직접 구현할 경우 예상치 못한 문제가 많이 발생할 수 있다. 직접 스마트 포인터를 구현하면서 다양한 문제를 겪고 해결하는 과정에서 많은 교훈을 얻었다.

C++ 멀티스레딩 환경에서 스마트 포인터를 사용할 때는 반드시 상호배제를 명확히 하고, Reference Count를 안전하게 관리해야 한다. 이 글이 같은 문제를 고민하는 개발자들에게 도움이 되었으면 좋겠다.

'C++ 개발이야기' 카테고리의 다른 글