8. ReaderWriterLockSlim 쓰기 기아 + 재진입 + 예외 누수
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMO 서버의 전역 세션 레지스트리(SessionRegistry)다.
// - 수십 개의 로직/네트워크 스레드가 "세션 조회"를 초당 수만 번 한다(읽기).
// - 가끔(접속/해제) 세션을 추가/제거한다(쓰기). 읽기가 압도적으로 많다.
// - "읽기 다중/쓰기 단독"이 맞다고 판단해 ReaderWriterLockSlim 을 도입.
//
// 운영 중 증상:
// - 평시엔 빠른데, 부하가 높아 읽기 트래픽이 포화되면 "접속/해제 처리가
// 수 초씩 지연"되거나 사실상 멈춘다. 새 플레이어가 한참 로비에 묶인다.
// - 동시에, 어떤 코드경로에서 같은 스레드가 락을 두 번 잡다 예외가 난다.
// - 또 다른 경로에선 예외 발생 시 "락이 안 풀려" 이후 모든 접근이 영구 블록.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 읽기는 동시 다중 허용, 쓰기는 단독.
// - 읽기 폭주 중에도 쓰기(접속/해제)가 "합리적 시간 안에" 처리돼야 한다.
// - 예외가 나도 락은 반드시 해제돼야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 쓰기가 왜 굶주리는가(starvation)? 어떤 정책 문제인가?
// 2) (A)(B)(C) 각 지점의 결함을 설명하라.
// 3) 기아·재진입·예외누수를 모두 해결하도록 수정하라.
// ============================================================================
using System;
using System.Collections.Generic;
using System.Threading;
namespace SessionReg
{
public sealed class Session
{
public int Id;
public string Name = "";
}
public sealed class SessionRegistry
{
// (A) 락 정책: 기본 생성자
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private readonly Dictionary<int, Session> _sessions = new();
public Session Find(int id)
{
_lock.EnterReadLock();
// (B) 읽는 동안 예외가 나면?
_sessions.TryGetValue(id, out var s);
// 검증/후처리에서 예외가 날 수 있는 호출
Validate(s);
_lock.ExitReadLock();
return s;
}
public void Add(Session s)
{
_lock.EnterWriteLock();
_sessions[s.Id] = s;
_lock.ExitWriteLock();
}
public void Remove(int id)
{
_lock.EnterWriteLock();
_sessions.Remove(id);
_lock.ExitWriteLock();
}
// 어떤 세션이 특정 조건이면, 같은 호출 흐름 안에서 추가 조회를 한다
public bool FindAndCheckParty(int id)
{
_lock.EnterReadLock();
_sessions.TryGetValue(id, out var s);
bool inParty = false;
if (s != null)
{
// (C) 읽기 락을 잡은 채로 같은 객체의 다른 조회 메서드를 또 호출
// -> 그 메서드도 EnterReadLock 을 시도한다
inParty = HasPartyMember(s.Id);
}
_lock.ExitReadLock();
return inParty;
}
private bool HasPartyMember(int id)
{
_lock.EnterReadLock(); // (C) 재진입 시도
bool exists = _sessions.ContainsKey(id);
_lock.ExitReadLock();
return exists;
}
private void Validate(Session s)
{
// 디버그 검증 등에서 가끔 throw
if (s != null && s.Id < 0)
throw new InvalidOperationException("bad session id");
}
}
} 결함 코드 · C++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMO 서버의 전역 세션 레지스트리(SessionRegistry)다.
// - 수십 개의 로직/네트워크 스레드가 "세션 조회"를 초당 수만 번 한다(읽기).
// - 가끔(접속/해제) 세션을 추가/제거한다(쓰기). 읽기가 압도적으로 많다.
// - "읽기 다중/쓰기 단독"이 맞다고 판단해 std::shared_mutex 를 도입.
//
// 운영 중 증상:
// - 평시엔 빠른데, 부하가 높아 읽기 트래픽이 포화되면 "접속/해제 처리가
// 수 초씩 지연"되거나 사실상 멈춘다. 새 플레이어가 한참 로비에 묶인다.
// - 동시에, 어떤 코드경로에서 같은 스레드가 락을 두 번 잡다 데드락/UB가 난다.
// - 또 다른 경로에선 예외 발생 시 "락이 안 풀려" 이후 모든 접근이 영구 블록.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 읽기는 동시 다중 허용, 쓰기는 단독.
// - 읽기 폭주 중에도 쓰기(접속/해제)가 "합리적 시간 안에" 처리돼야 한다.
// - 예외가 나도 락은 반드시 해제돼야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 쓰기가 왜 굶주리는가(starvation)? 어떤 정책 문제인가?
// 2) (A)(B)(C) 각 지점의 결함을 설명하라.
// 3) 기아·재진입·예외누수를 모두 해결하도록 수정하라.
// ============================================================================
#include <shared_mutex>
#include <stdexcept>
#include <string>
#include <unordered_map>
struct Session
{
int Id = 0;
std::string Name;
};
class SessionRegistry
{
public:
Session* Find(int id)
{
// (A) 락 정책: 표준 shared_mutex는 reader/writer 우선순위를 지정할 수 없다
lock_.lock_shared();
// (B) 읽는 동안 예외가 나면? unlock_shared가 호출되지 않는다
Session* s = nullptr;
auto it = sessions_.find(id);
if (it != sessions_.end())
s = &it->second;
// 검증/후처리에서 예외가 날 수 있는 호출
Validate(s);
lock_.unlock_shared();
return s;
}
void Add(const Session& s)
{
lock_.lock();
sessions_[s.Id] = s;
lock_.unlock();
}
void Remove(int id)
{
lock_.lock();
sessions_.erase(id);
lock_.unlock();
}
// 어떤 세션이 특정 조건이면, 같은 호출 흐름 안에서 추가 조회를 한다
bool FindAndCheckParty(int id)
{
lock_.lock_shared();
auto it = sessions_.find(id);
bool inParty = false;
if (it != sessions_.end())
{
// (C) 읽기 락을 잡은 채로 같은 객체의 다른 조회 메서드를 또 호출
// -> 그 메서드도 lock_shared() 를 시도한다
inParty = HasPartyMember(it->second.Id);
}
lock_.unlock_shared();
return inParty;
}
private:
bool HasPartyMember(int id)
{
lock_.lock_shared(); // (C)
bool exists = sessions_.find(id) != sessions_.end();
lock_.unlock_shared();
return exists;
}
void Validate(Session* s)
{
// 디버그 검증 등에서 가끔 throw
if (s != nullptr && s->Id < 0)
throw std::runtime_error("bad session id");
}
std::shared_mutex lock_;
std::unordered_map<int, Session> sessions_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.