37. 인접 테이블의 락 없는 순회와 손상된 열거자 (읽기는 안전하다는 착각)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 존 인접 리스트(broadcast 대상) 캐시
// ----------------------------------------------------------------------------
// 시나리오 (서버 내부):
// - 존 서버는 플레이어가 반경 안에 들어오고 나갈 때마다 인접 테이블
// (Dictionary<long, PlayerHandle>)에 삽입/제거한다(이동 스레드, 틱마다 호출).
// - 브로드캐스트 스레드는 주기적으로 인접 테이블 전체를 훑어 스냅샷 패킷을
// 각 플레이어에게 보낸다.
// - 삽입/제거(Enter/Leave)는 반드시 락으로 보호하지만, 브로드캐스트는
// "값을 바꾸는 게 아니라 읽기만 하니 락이 필요 없다"고 보고 락 없이
// 그냥 foreach 로 순회한다.
// - 동접이 늘면서 이동이 잦아졌고, 브로드캐스트 도중 서버가 예외로
// 죽거나 스킵되는 로그가 간헐적으로 보고된다.
//
// 과제:
// Enter/Leave(A)와 Broadcast(B)를 각각 검토하라. 두 스레드가 동시에
// 실행될 때 (B)에서 실제로 무슨 일이 벌어질 수 있는지, 그리고 이 문제가
// "운이 나쁘면 가끔" 발생하는 이유를 설명하라. 수정안을 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System.Collections.Generic;
namespace GameServer.Zone
{
public sealed class PlayerHandle
{
public long Id;
public float X, Y;
}
public sealed class NeighborTable
{
private readonly object _lock = new object();
private readonly Dictionary<long, PlayerHandle> _neighbors = new Dictionary<long, PlayerHandle>();
// 이동/입퇴장 스레드에서 호출
public void Enter(PlayerHandle p)
{
lock (_lock) // (A) 삽입은 락으로 보호
{
_neighbors[p.Id] = p;
}
}
public void Leave(long id)
{
lock (_lock) // (A) 제거도 락으로 보호
{
_neighbors.Remove(id);
}
}
// 브로드캐스트 스레드에서 주기적으로(예: 100ms) 호출
public void BroadcastSnapshot()
{
// 조회뿐이니 락 없이 순회해도 안전하다고 가정
foreach (var kv in _neighbors) // (B)
{
PlayerHandle p = kv.Value;
SendSnapshotPacket(p);
}
}
private void SendSnapshotPacket(PlayerHandle p)
{
// 네트워크 전송 (생략)
}
}
} 결함 코드 · C++
// ─────────────────────────────────────────────────────────────
// 시나리오 (서버 내부): 존 인접 리스트(broadcast 대상) 캐시
// - 존 서버는 플레이어가 반경 안에 들어오고 나갈 때마다 인접 테이블
// (std::unordered_map<int64_t, PlayerHandle>)에 삽입/제거한다
// (이동 스레드, 틱마다 호출).
// - 브로드캐스트 스레드는 주기적으로 인접 테이블 전체를 훑어 스냅샷
// 패킷을 각 플레이어에게 보낸다.
// - 삽입/제거(enter/leave)는 mutex로 보호하지만, 브로드캐스트는
// "값을 바꾸는 게 아니라 읽기만 하니 락이 필요 없다"고 보고 락 없이
// 그냥 range-for 로 순회한다.
// - 동접이 늘면서 이동이 잦아졌고, 브로드캐스트 스레드가 간헐적으로
// 크래시하거나(주로 SIGSEGV) 이상한 플레이어에게 패킷을 보내는
// 현상이 보고된다.
//
// 과제: enter/leave(A)와 broadcastSnapshot(B)를 검토하라. 두 스레드가
// 동시에 실행될 때 (B)의 순회 도중 (A)의 삽입이 끼어들면 무슨 일이
// 벌어지는지 설명하고, 이것이 "항상"이 아니라 "간헐적으로" 크래시하는
// 이유도 설명하라. 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
#include <cstdint>
#include <mutex>
#include <unordered_map>
namespace zone {
struct PlayerHandle {
int64_t id = 0;
float x = 0.f, y = 0.f;
};
class NeighborTable {
public:
// 이동/입퇴장 스레드에서 호출
void enter(const PlayerHandle& p) {
std::lock_guard<std::mutex> lk(mtx_); // (A) 삽입은 락으로 보호
neighbors_[p.id] = p;
}
void leave(int64_t id) {
std::lock_guard<std::mutex> lk(mtx_); // (A) 제거도 락으로 보호
neighbors_.erase(id);
}
// 브로드캐스트 스레드에서 주기적으로(예: 100ms) 호출
void broadcastSnapshot() {
// 조회뿐이니 락 없이 순회해도 안전하다고 가정
for (auto& kv : neighbors_) { // (B)
const PlayerHandle& p = kv.second;
sendSnapshotPacket(p);
}
}
private:
void sendSnapshotPacket(const PlayerHandle&) {
// 네트워크 전송 (생략)
}
std::mutex mtx_;
std::unordered_map<int64_t, PlayerHandle> neighbors_;
};
} // namespace zone 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.