41. 락프리 스냅샷 교체의 유예 기간 부재: 풀 조기 반납 후 재사용 오염 (C#)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오: 게임 서버가 자주 읽히고 가끔 통째로 교체되는 "월드 스냅샷"을 공유한다.
// 갱신 스레드(틱)는 새 스냅샷을 만들어 참조를 교체(publish)하고, 읽기 스레드(수십 개
// 세션 워커)는 현재 참조를 읽어 여러 필드를 연달아 사용한다. 할당/GC 압박을 줄이려고
// 스냅샷 객체는 풀(pool)에서 빌려 쓰고, 교체 시 옛 스냅샷은 풀로 돌려준다.
//
// 요구사항:
// - 읽기 스레드는 lock 없이 저비용으로 현재 스냅샷을 읽는다.
// - 스냅샷 교체가 읽기 스레드의 진행 중인 참조를 깨뜨리면 안 된다.
// - 풀로 반납된 스냅샷은 아무도 더는 사용하지 않아야 재사용해도 안전하다.
//
// 과제: Publish 와 ReadTick(그리고 반납 시점)을 검토하고, 읽기 스레드가 무엇을 관측할 수
// 있는지, 어떤 조건에서 드러나는지, 어떻게 고칠지 설명하라. (A)(B)(C) 를 살펴보라.
using System.Threading;
public sealed class WorldSnapshot { public long Tick; public double EventMultiplier; public void Reset() { Tick = 0; EventMultiplier = 0; } }
public sealed class SnapshotPool
{
public WorldSnapshot Acquire() { /* 풀에서 하나 꺼내 필드 채워 반환 */ return new WorldSnapshot(); }
public void Release(WorldSnapshot s) { s.Reset(); /* 풀에 되돌려 재사용 대기 */ } // 이후 Acquire 로 재사용됨
}
public sealed class SnapshotStore
{
private WorldSnapshot _current;
private readonly SnapshotPool _pool = new SnapshotPool();
// 틱 스레드 1개가 호출: 새 스냅샷을 게시하고 옛 것을 풀로 반납한다.
public void Publish(WorldSnapshot fresh)
{
var old = Interlocked.Exchange(ref _current, fresh); // (A)
if (old != null) _pool.Release(old); // (B)
}
// 여러 세션 워커가 동시에 호출: 현재 스냅샷의 필드들을 연달아 사용한다.
public long ReadTick()
{
var s = Volatile.Read(ref _current);
// ... 여기서 s 의 여러 필드를 연달아 사용하는 처리(생략) ...
return s?.Tick ?? 0; // (C)
}
} 결함 코드 · C++
// 시나리오: 게임 서버가 자주 읽히고 가끔 통째로 교체되는 "월드 스냅샷"을 공유한다.
// 갱신 스레드(틱)는 새 스냅샷을 만들어 원자적 포인터로 게시(publish)하고, 읽기 스레드
// (수십 개 세션 워커)는 현재 포인터를 읽어 여러 필드를 연달아 참조한다. 할당을 줄이려고
// 스냅샷 객체는 풀(pool)에서 빌려 쓰고, 교체 시 옛 스냅샷은 풀로 돌려준다.
//
// 요구사항:
// - 읽기 스레드는 락 없이 저비용으로 현재 스냅샷을 읽는다.
// - 스냅샷 교체가 읽기 스레드의 진행 중인 참조를 깨뜨리면 안 된다.
// - 풀로 반납된 스냅샷은 아무도 더는 참조하지 않아야 재사용/파괴해도 안전하다.
//
// 과제: publish 와 readTick(그리고 반납 시점)을 검토하고, 읽기 스레드가 무엇을 관측·접근할
// 수 있는지, 어떤 조건에서 드러나는지, 어떻게 고칠지 설명하라. (A)(B)(C) 를 살펴보라.
#include <atomic>
#include <cstdint>
struct WorldSnapshot { uint64_t tick; double eventMultiplier; /* 그 외 큰 상태 */ };
struct Pool {
WorldSnapshot* acquire();
void release(WorldSnapshot* p); // 반납된 객체는 이후 다른 acquire 로 재사용/덮어써짐
};
class SnapshotStore {
public:
// 틱 스레드 1개가 호출: 새 스냅샷을 게시하고 옛 것을 풀로 반납한다.
void publish(WorldSnapshot* fresh) {
WorldSnapshot* old = current_.exchange(fresh, std::memory_order_acq_rel); // (A)
if (old) pool_.release(old); // (B)
}
// 여러 세션 워커가 동시에 호출: 현재 스냅샷의 필드들을 연달아 읽는다.
uint64_t readTick() {
WorldSnapshot* s = current_.load(std::memory_order_acquire);
// ... 여기서 s 의 여러 필드를 연달아 참조하는 처리(생략) ...
return s ? s->tick : 0; // (C)
}
private:
std::atomic<WorldSnapshot*> current_{nullptr};
Pool pool_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.