31. 엔티티 핸들(slot map)의 세대 검증 부재·비동기 재사용으로 인한 오접근(ABA형) (C#)
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오: 월드 엔티티를 슬롯맵(slot map)으로 관리한다. 엔티티는 _slots 배열에 저장되고,
// 외부 코드는 핸들 Handle{Index, Generation} 로 엔티티를 참조한다. 엔티티가 디스폰되면
// 슬롯은 비워지고 Generation 이 증가하며, 그 슬롯은 다음 스폰에 재사용된다.
// 스폰/디스폰은 스폰·디스폰 이벤트 스레드가, Resolve() 는 게임플레이/네트워크 스레드가 호출한다.
//
// 요구사항:
// - 오래된(stale) 핸들 — 가리키던 엔티티가 이미 디스폰된 핸들 — 로 Resolve 하면
// 반드시 null 을 반환해야 한다(다른 엔티티를 돌려주면 안 됨).
// - Resolve 는 스폰/디스폰과 동시에 호출될 수 있다.
//
// 과제: (A)(B) 지점을 검토하라. 특정 순서에서 Resolve 가 의도와 다른 결과를 주거나
// 일관되지 않은 상태를 읽을 수 있다. 어떤 순서에서 그런지 밝히고 고쳐라.
using System.Collections.Generic;
public sealed class Entity { public int Hp; public float X, Y; }
public struct Handle { public uint Index; public uint Generation; }
public sealed class EntityRegistry
{
private struct Slot { public uint Generation; public bool Alive; public Entity Entity; }
private readonly Slot[] _slots;
private readonly Stack<uint> _freeList = new();
public EntityRegistry(uint capacity)
{
_slots = new Slot[capacity];
for (uint i = capacity; i-- > 0; ) _freeList.Push(i);
}
public Handle Spawn() // 스폰 이벤트 스레드
{
uint idx = _freeList.Pop();
_slots[idx].Alive = true;
_slots[idx].Entity = new Entity();
return new Handle { Index = idx, Generation = _slots[idx].Generation };
}
public void Despawn(Handle h) // 디스폰 이벤트 스레드
{
_slots[h.Index].Alive = false;
_slots[h.Index].Generation++; // 재사용을 위해 세대 증가
_freeList.Push(h.Index);
}
public Entity? Resolve(Handle h) // 게임플레이/네트워크 스레드
{
ref Slot s = ref _slots[h.Index];
if (s.Alive) // (A)
return s.Entity; // (B)
return null;
}
} 결함 코드 · C++
// 시나리오: 월드 엔티티를 슬롯맵(slot map)으로 관리한다. 엔티티는 slots_ 배열에 저장되고,
// 외부 코드는 핸들 Handle{index, generation} 로 엔티티를 참조한다. 엔티티가 디스폰되면
// 슬롯은 비워지고 generation 이 증가하며, 그 슬롯은 다음 스폰에 재사용된다.
// 스폰/디스폰은 스폰·디스폰 이벤트 스레드가, resolve() 는 게임플레이/네트워크 스레드가 호출한다.
//
// 요구사항:
// - 오래된(stale) 핸들 — 가리키던 엔티티가 이미 디스폰된 핸들 — 로 resolve 하면
// 반드시 nullptr 를 반환해야 한다(다른 엔티티를 돌려주면 안 됨).
// - resolve 는 스폰/디스폰과 동시에 호출될 수 있다.
//
// 과제: (A)(B) 지점을 검토하라. 특정 순서에서 resolve 가 의도와 다른 결과를 주거나
// 잘못된 메모리를 건드릴 수 있다. 어떤 순서에서 그런지 밝히고 고쳐라.
#include <cstdint>
#include <vector>
struct Entity { int hp = 0; float x = 0, y = 0; };
struct Handle { uint32_t index = 0; uint32_t generation = 0; };
class EntityRegistry {
public:
explicit EntityRegistry(uint32_t capacity) : slots_(capacity) {
for (uint32_t i = capacity; i-- > 0; ) freeList_.push_back(i);
}
Handle spawn() { // 스폰 이벤트 스레드
uint32_t idx = freeList_.back();
freeList_.pop_back();
Slot& s = slots_[idx];
s.alive = true;
s.entity = Entity{};
return Handle{ idx, s.generation };
}
void despawn(Handle h) { // 디스폰 이벤트 스레드
Slot& s = slots_[h.index];
s.alive = false;
++s.generation; // 재사용을 위해 세대 증가
freeList_.push_back(h.index);
}
Entity* resolve(Handle h) { // 게임플레이/네트워크 스레드
Slot& s = slots_[h.index];
if (s.alive) // (A)
return &s.entity; // (B)
return nullptr;
}
private:
struct Slot { uint32_t generation = 0; bool alive = false; Entity entity; };
std::vector<Slot> slots_;
std::vector<uint32_t> freeList_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.