30. Reader-Writer 락 오용: 읽기 락 보유 중 쓰기 승급과 순회 중 변경 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ─────────────────────────────────────────────────────────────
// 시나리오: 존(zone)의 엔티티 맵은 읽기(검색·주변 조회)가 압도적으로 많고,
// 쓰기(스폰/디스폰)는 가끔이라 ReaderWriterLockSlim으로 보호한다.
// - Find: 읽기 락으로 단건 조회.
// - ScanAndCleanup: 주기적으로 전체를 훑어 죽은 엔티티를 제거.
// - Despawn: 단건 제거(콜백 호출 포함).
// 여러 워커 스레드가 이 메서드들을 동시에 호출한다.
//
// 요구사항:
// - 읽기는 동시에 여러 개가 진행되어도 되고, 쓰기는 배타적이어야 한다.
// - 어떤 경로(예외 포함)로 빠져나가도 락이 반드시 풀려야 하고, 스레드들이 서로를 무한히 기다리는 상황이 없어야 한다.
//
// 과제: (A)(B) 지점을 검토하라. ScanAndCleanup이 죽은 엔티티를 만났을 때,
// 그리고 Despawn 도중 예외가 났을 때 무슨 일이 벌어지는지 설명하고
// 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
using System.Collections.Generic;
using System.Threading;
namespace World
{
public sealed class Entity { public long Id; public bool IsDead; }
public sealed class Zone
{
private readonly ReaderWriterLockSlim _lock = new();
private readonly Dictionary<long, Entity> _entities = new();
public Entity? Find(long id)
{
_lock.EnterReadLock();
try { return _entities.TryGetValue(id, out var v) ? v : null; }
finally { _lock.ExitReadLock(); }
}
public void Spawn(Entity e)
{
_lock.EnterWriteLock();
try { _entities[e.Id] = e; }
finally { _lock.ExitWriteLock(); }
}
public void ScanAndCleanup()
{
_lock.EnterReadLock();
// (A)
foreach (var kv in _entities)
{
if (kv.Value.IsDead)
{
_lock.EnterWriteLock();
_entities.Remove(kv.Key);
_lock.ExitWriteLock();
}
}
_lock.ExitReadLock();
}
public void Despawn(long id)
{
_lock.EnterWriteLock();
// (B)
var e = _entities[id]; // 키 없으면 예외
OnDespawn(e); // 콜백에서 예외가 날 수 있음
_entities.Remove(id);
_lock.ExitWriteLock();
}
private void OnDespawn(Entity e) { /* 드롭/로그/이벤트 발행(예외 가능) */ }
}
} 결함 코드 · C++
// ─────────────────────────────────────────────────────────────
// 시나리오: 존(zone)의 엔티티 맵은 읽기가 압도적이고 쓰기는 가끔이라
// std::shared_mutex 로 보호한다.
// - find: 공유(읽기) 락으로 단건 조회(복사 반환).
// - scanAndCleanup: 전체를 훑어 죽은 엔티티 제거.
// - despawn: 단건 제거(콜백 호출 포함).
// 여러 워커 스레드가 동시에 호출한다.
//
// 요구사항:
// - 읽기는 동시 다중 허용, 쓰기는 배타적. 어떤 경로(예외 포함)로 빠져나가도
// 락이 반드시 풀려야 하고, 스레드들이 서로를 무한히 기다리는 상황이 없어야 한다.
//
// 과제: (A)(B) 지점을 검토하라. scanAndCleanup이 죽은 엔티티를 만났을 때,
// 그리고 despawn 도중 예외가 났을 때 무슨 일이 벌어지는지 설명하고 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
#include <cstdint>
#include <optional>
#include <shared_mutex>
#include <mutex>
#include <unordered_map>
namespace world {
struct Entity { int64_t id = 0; bool dead = false; };
class Zone {
public:
std::optional<Entity> find(int64_t id) {
std::shared_lock<std::shared_mutex> lk(mtx_);
auto it = entities_.find(id);
if (it == entities_.end()) return std::nullopt;
return it->second;
}
void spawn(const Entity& e) {
std::unique_lock<std::shared_mutex> lk(mtx_);
entities_[e.id] = e;
}
void scanAndCleanup() {
std::shared_lock<std::shared_mutex> lk(mtx_); // 공유(읽기) 락
// (A)
for (auto& kv : entities_) {
if (kv.second.dead) {
std::unique_lock<std::shared_mutex> wl(mtx_); // 같은 mutex 독점 락
entities_.erase(kv.first);
}
}
}
void despawn(int64_t id) {
mtx_.lock(); // 수동 독점 락
// (B)
Entity& e = entities_.at(id); // 없으면 throw
onDespawn(e); // 콜백에서 예외 가능
entities_.erase(id);
mtx_.unlock();
}
private:
void onDespawn(Entity&) { /* 드롭/로그/이벤트(예외 가능) */ }
std::shared_mutex mtx_;
std::unordered_map<int64_t, Entity> entities_;
};
} // namespace world 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.