45. 지연 삭제 큐의 중복 등록과 풀 이중 반납 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// [시나리오]
// 존(zone) 서버의 틱 루프. 엔티티를 틱 도중에 즉시 파괴하면 순회 중인 시스템들이 깨지므로,
// 삭제 요청을 큐에 모아 두었다가 틱 끝에서 한꺼번에 처리하는 "지연 삭제(deferred delete)"를
// 쓴다. 엔티티 객체는 GC 압력을 줄이기 위해 오브젝트 풀에서 대여/반납한다.
//
// 삭제 요청은 여러 경로에서 들어올 수 있다.
// - 체력이 0이 되어 사망 처리
// - 지속시간이 끝난 소환수/투사체의 만료
// - 플레이어 접속 종료
// - 존 이동으로 인한 디스폰
// 이 경로들은 서로를 알지 못하며, 같은 틱에 같은 엔티티에 대해 각각 삭제를 요청할 수 있다.
// 일부 경로는 틱 스레드가 아닌 워커 스레드(네트워크 I/O 완료 등)에서 호출된다.
//
// [요구사항]
// - 한 엔티티는 정확히 한 번만 파괴되고, 정확히 한 번만 풀에 반납된다.
// - 파괴 이벤트(OnEntityDestroyed)는 엔티티당 1회만 발행된다.
// - 삭제 예약된 엔티티는 다음 틱부터 조회되지 않는다.
//
// [과제]
// (A), (B) 지점을 중심으로 요구사항이 깨지는 조건을 찾고, 재현 시나리오와 수정안을
// 제시하시오.
using System;
using System.Collections.Generic;
namespace GameServer.Zone
{
public sealed class Entity
{
public int Id;
public bool Alive = true;
public object State;
public void Reset() { Id = 0; Alive = true; State = null; }
}
public sealed class EntityPool
{
private readonly Stack<Entity> _free = new();
public Entity Rent() => _free.Count > 0 ? _free.Pop() : new Entity();
public void Return(Entity e) { e.Reset(); _free.Push(e); }
}
public sealed class ZoneWorld
{
private readonly Dictionary<int, Entity> _entities = new();
private readonly List<Entity> _pendingDelete = new();
private readonly EntityPool _pool = new();
public event Action<int> OnEntityDestroyed;
public Entity Find(int id) => _entities.TryGetValue(id, out var e) ? e : null;
// 사망/만료/접속종료/디스폰 등 여러 경로에서 호출된다.
public void RequestDelete(int id)
{
var e = Find(id);
if (e == null) return;
// (A)
_pendingDelete.Add(e);
}
// 틱 끝에서 호출된다.
public void FlushDeletions()
{
// (B)
foreach (var e in _pendingDelete)
{
e.Alive = false;
_entities.Remove(e.Id);
OnEntityDestroyed?.Invoke(e.Id);
_pool.Return(e);
}
_pendingDelete.Clear();
}
}
} 결함 코드 · C++
// [시나리오]
// 존(zone) 서버의 틱 루프. 틱 도중 즉시 파괴하면 순회 중인 시스템이 깨지므로, 삭제 요청을
// 큐에 모아 두었다가 틱 끝에서 일괄 처리하는 "지연 삭제(deferred delete)"를 쓴다.
// 엔티티는 할당 비용을 줄이려고 오브젝트 풀에서 대여/반납한다.
//
// 삭제 요청은 서로를 모르는 여러 경로에서 들어온다(사망 처리, 소환물 만료, 접속 종료,
// 존 이동 디스폰). 일부 경로는 틱 스레드가 아닌 워커 스레드에서 호출된다.
//
// [요구사항]
// - 한 엔티티는 정확히 한 번만 파괴되고, 정확히 한 번만 풀에 반납된다.
// - 파괴 콜백은 엔티티당 1회만 호출된다.
// - 삭제 예약된 엔티티는 다음 틱부터 조회되지 않는다.
//
// [과제]
// (A), (B) 지점을 중심으로 요구사항이 깨지는 조건을 찾고, 재현 시나리오와 수정안을
// 제시하시오.
#include <cstdint>
#include <functional>
#include <mutex>
#include <unordered_map>
#include <vector>
namespace gs {
struct Entity {
int id = 0;
bool alive = true;
void* state = nullptr;
void reset() { id = 0; alive = true; state = nullptr; }
};
class EntityPool {
public:
Entity* rent() {
if (free_.empty()) return new Entity();
Entity* e = free_.back();
free_.pop_back();
return e;
}
void ret(Entity* e) {
e->reset();
free_.push_back(e);
}
private:
std::vector<Entity*> free_;
};
class ZoneWorld {
public:
std::function<void(int)> onEntityDestroyed;
Entity* find(int id) {
auto it = entities_.find(id);
return it == entities_.end() ? nullptr : it->second;
}
// 사망/만료/접속종료/디스폰 등 여러 경로에서 호출된다.
void requestDelete(int id) {
Entity* e = find(id);
if (e == nullptr) return;
// (A)
pendingDelete_.push_back(e);
}
// 틱 끝에서 호출된다.
void flushDeletions() {
// (B)
for (Entity* e : pendingDelete_) {
e->alive = false;
entities_.erase(e->id);
if (onEntityDestroyed) onEntityDestroyed(e->id);
pool_.ret(e);
}
pendingDelete_.clear();
}
private:
std::unordered_map<int, Entity*> entities_;
std::vector<Entity*> pendingDelete_;
EntityPool pool_;
};
} // namespace gs
int main() { return 0; } 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.