레퍼런스 카운팅

Jaemyeong Lee·2025년 3월 22일

게임 서버1

목록 보기
145/220

왜 레퍼런스 카운팅이 필요한가

IOCP 완료 지연의 본질

  • I/O 등록(WSARecv, WSASend)과 완료 통지(GQCS) 사이에는 시간이 있습니다.
  • 이 사이에 Session이 해제되면, 늦게 도착한 완료가 해제된 객체를 참조해 크래시가 납니다.

대표 크래시 타임라인

  1. Session이 Recv I/O 등록
  2. 사용자 종료로 Session 삭제 시작
  3. 커널에서 해당 I/O 완료가 큐에 도착
  4. 워커가 완료를 꺼내 Session* 접근 -> use-after-free

결론

"등록된 I/O가 1개라도 남아 있으면 Session은 살아 있어야 한다."


owner shared_ptr 패턴

기본 전략

  • 이벤트(IocpEvent)에 owner(shared_ptr<Session>)를 붙여 수명을 고정합니다.
  • 등록 시 ref 증가, 완료 처리 마지막에 ref 해제 규칙을 강제합니다.

등록/해제 예시

bool Session::RegisterRecv()
{
    _recvEvent.owner = shared_from_this(); // +1

    DWORD flags = 0;
    DWORD bytes = 0;
    int32 ret = WSARecv(_socket, &_recvEvent.wsaBuf, 1, &bytes, &flags,
                        &_recvEvent.overlapped, nullptr);

    if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
    {
        _recvEvent.owner.reset(); // 실패 시 즉시 해제
        return false;
    }
    return true;
}

void Session::ProcessRecv(RecvEvent* ev, int32 numOfBytes)
{
    // ... 처리 ...
    ev->owner.reset(); // 완료 처리 경로에서 해제
}

핵심 규칙

  • 성공 경로뿐 아니라 실패/취소/종료 경로에서도 owner 해제를 보장해야 합니다.
  • GQCS FALSE && ov != nullptr인 실패 완료도 "완료 이벤트"이므로 ref 해제 대상입니다.

shared_from_this와 이중 소유권 함정

반드시 지켜야 할 규칙

  • Session 생성은 make_shared<Session>() 같은 단일 control block으로 시작합니다.
  • 내부에서 자신 참조가 필요하면 shared_from_this()를 사용합니다.

금지 패턴

Session* raw = this;
std::shared_ptr<Session> wrong(raw); // 금지: 별도 control block 생성 위험
  • 이런 코드는 이중 해제/메모리 오염의 원인이 됩니다.

생성자 시점 주의

  • 생성자/소멸자 내부에서 shared_from_this() 호출은 안전하지 않을 수 있습니다.
  • 객체가 실제 shared_ptr에 소유된 이후 시점에서만 호출하세요.

순환 참조(leak)도 같이 막아야 한다

흔한 누수 시나리오

  • Session이 Event를 멤버로 소유
  • Event의 owner가 Session을 shared_ptr로 참조
  • 완료 후 owner.reset() 누락 시 self-reference가 남아 세션이 해제되지 않음

방지 방법

방법장점주의
완료 시 owner.reset() 강제단순하고 직관적모든 경로에서 누락 없이 수행
weak_ptr + 필요 시 lock순환 참조 위험 감소lock 실패 경로 처리 필요
별도 pendingIoCount refcount명시적 수명 모델구현 난이도 증가

실무 팁

  • owner 해제 누락을 막기 위해 처리 함수를 템플릿화하거나 공통 후처리 헬퍼를 두는 것이 좋습니다.

종료 시나리오와 late completion

종료 중 자주 나오는 완료

  • ERROR_OPERATION_ABORTED
  • ERROR_NETNAME_DELETED

이 값들은 종료 과정에서 예상 가능한 경로일 수 있습니다.

종료 순서 권장

  1. Disconnecting 상태 전환 (신규 I/O 등록 금지)
  2. 소켓 shutdown/close
  3. 늦게 도착한 완료 처리(정리 경로)
  4. outstanding ref가 0이 되면 최종 해제

Session과의 연결

  • Session 상태 전이와 refcount 정책을 함께 설계해야 중복 종료/댕글링을 동시에 막을 수 있습니다.

shared_ptr의 한계

무엇을 보장하는가

  • 참조 카운트 증감의 스레드 안전성
  • 객체 생명주기 연장

무엇을 보장하지 않는가

  • Session 내부 상태 변경의 동기화
  • Send/Recv 큐의 경쟁 상태 해결

의미

  • shared_ptr는 수명 문제 해결 도구이지, 동시성 문제 전체 해결 도구가 아닙니다.

강의 시 유의사항

강조 포인트

  • Part 6의 핵심은 "메모리 해제 시점 제어"입니다.
  • IOCP는 늦게 오는 완료가 기본이므로, 수명 보장은 선택이 아니라 필수입니다.
  • "크래시 방지(댕글링) + 누수 방지(순환 참조)" 두 축을 함께 가르쳐야 합니다.

자주 하는 오해

오해바로잡기
shared_ptr 쓰면 모든 문제가 해결된다수명만 해결, 상태 동기화는 별도 설계 필요
owner는 성공 경로에서만 해제하면 된다실패 완료/취소 경로에서도 반드시 해제
Completion Key만 안전하면 충분하다Event owner와 종료 순서가 함께 맞아야 안전

체크 질문 (스스로 답해보기)

  • GQCS FALSE && ov != nullptr에서 owner를 해제하지 않으면 어떤 문제가 생기는가?
  • Session 멤버 Event의 owner를 reset하지 않으면 왜 누수가 발생할 수 있는가?
  • Disconnect 중 late completion을 안전하게 처리하려면 상태 전이와 refcount를 어떻게 결합할 것인가?

profile
李家네_공부방

0개의 댓글