44. 버프 슬롯 비트마스크 CAS 루프의 재시도 결함 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMORPG 존(Zone) 서버의 "버프 슬롯 레지스트리"다. 플레이어마다 32칸짜리 버프
// 슬롯이 있고, 사용 중인 칸을 int 비트마스크 하나(SlotMask)로 관리한다. 버프 부여
// 요청은 IOCP 워커 스레드 여러 개에서 동시에 들어온다(광역 힐, 파티 오라, 필드
// 장판이 같은 tick 에 겹친다). 또 하나의 공유 상태로 "인스턴스 던전 입장 토큰"이
// 있어 존이 동시에 만들 수 있는 인스턴스 수를 제한한다. 둘 다 초당 수만 번
// 갱신되므로 lock 없이 Interlocked 기반 CAS 루프로 구현되어 있다.
//
// 요구사항: R1 슬롯당 버프 1개 R2 마스크에 반영된 경우에만 통계·이벤트 정확히 1회
// R3 토큰 잔량 음수 금지, 발급 로그 건수 = 실제 발급 수
// R4 부하 시에도 워커 스레드가 코어를 독점하지 않을 것
//
// 과제: (A)/(B) 마커 지점을 중심으로 무엇이 잘못되었는지, 어떤 상황에서 요구사항이
// 깨지는지, 어떻게 고칠지 설명하라.
// ============================================================================
using System;
using System.Collections.Concurrent;
using System.Threading;
namespace ZoneServer.Buffs
{
public struct BuffPayload { public int BuffId; public int Stack; public long ExpireTicks; }
public sealed class BuffAppliedEvent
{
public long PlayerId; public int Slot; public int MaskAfter; public DateTime AtUtc;
}
public sealed class TokenGrant
{
public long RequestId; public int Cost; public long RemainAfter;
}
public sealed class ZoneMetrics
{
private long _buffApplied, _buffRejected, _tokenGranted;
public void IncBuffApplied() => Interlocked.Increment(ref _buffApplied);
public void IncBuffRejected() => Interlocked.Increment(ref _buffRejected);
public void IncTokenGranted() => Interlocked.Increment(ref _tokenGranted);
public long BuffApplied => Interlocked.Read(ref _buffApplied);
public long BuffRejected => Interlocked.Read(ref _buffRejected);
public long TokenGranted => Interlocked.Read(ref _tokenGranted);
}
public sealed class PlayerBuffState
{
public long PlayerId;
public int SlotMask;
public readonly BuffPayload[] Payloads = new BuffPayload[32];
}
public sealed class BuffRegistry
{
private readonly ConcurrentDictionary<long, PlayerBuffState> _states = new();
private readonly ConcurrentQueue<BuffAppliedEvent> _events = new();
private readonly ConcurrentQueue<TokenGrant> _grantLog = new();
private readonly ZoneMetrics _metrics = new();
private long _entryTokens = 64;
private long _nextRequestId;
public bool TryApplyBuff(long playerId, int slot, BuffPayload payload)
{
if (slot < 0 || slot >= 32) return false;
var st = _states.GetOrAdd(playerId, id => new PlayerBuffState { PlayerId = id });
int bit = 1 << slot;
int observed = st.SlotMask; // (A)
while (true)
{
if ((observed & bit) != 0)
{
_metrics.IncBuffRejected();
return false;
}
int desired = observed | bit;
st.Payloads[slot] = payload;
_metrics.IncBuffApplied(); // (B)
_events.Enqueue(new BuffAppliedEvent
{
PlayerId = playerId, Slot = slot,
MaskAfter = desired, AtUtc = DateTime.UtcNow,
});
int prev = Interlocked.CompareExchange(ref st.SlotMask, desired, observed);
if (prev == observed)
return true;
// 다른 스레드가 마스크를 바꾼 경우 다시 시도한다.
}
}
public bool TryAcquireEntryToken(int cost)
{
if (cost <= 0) return false;
long observed = _entryTokens;
while (true)
{
if (observed < cost)
return false;
long desired = observed - cost;
long requestId = Interlocked.Increment(ref _nextRequestId);
_metrics.IncTokenGranted(); // (B)
_grantLog.Enqueue(new TokenGrant
{
RequestId = requestId, Cost = cost, RemainAfter = desired,
});
long prev = Interlocked.CompareExchange(ref _entryTokens, desired, observed);
if (prev == desired) // (A)
return true;
observed = _entryTokens;
}
}
public void ReleaseEntryToken(int cost) => Interlocked.Add(ref _entryTokens, cost);
public bool TryClearBuff(long playerId, int slot)
{
if (!_states.TryGetValue(playerId, out var st)) return false;
int bit = 1 << slot;
int observed = st.SlotMask;
int desired = observed & ~bit;
return Interlocked.CompareExchange(ref st.SlotMask, desired, observed) == observed;
}
}
} 결함 코드 · C++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMORPG 존(Zone) 서버의 "버프 슬롯 레지스트리"다. 플레이어마다 32칸짜리 버프
// 슬롯이 있고, 사용 중인 칸을 uint32_t 비트마스크 하나(slot_mask)로 관리한다.
// 슬롯의 실제 내용(BuffPayload)은 별도 배열에 있고, 마스크 비트가 서면 그때부터
// 다른 스레드가 그 슬롯을 읽어도 된다는 규약이다. 버프 부여는 네트워크 워커
// 스레드 여러 개에서 동시에 들어온다. 또 하나의 공유 상태로 "인스턴스 던전 생성
// 토큰"이 있어 존이 동시에 만들 수 있는 인스턴스 수를 제한한다. 둘 다 초당 수만
// 번 갱신되므로 mutex 없이 std::atomic CAS 로 구현되어 있다.
//
// 요구사항: R1 슬롯당 버프 1개
// R2 마스크에 반영된 경우에만 통계·이벤트가 정확히 1회 발행
// R3 마스크 비트를 본 스레드는 반드시 완성된 payload 를 본다
// R4 토큰 잔량 음수 금지, 발급 로그 건수 = 실제 발급 수
//
// 과제: (A)/(B) 마커 지점을 중심으로 무엇이 잘못되었는지, 어떤 상황에서
// 요구사항이 깨지는지, 어떻게 고칠지 설명하라.
// ============================================================================
#include <atomic>
#include <cstdint>
#include <deque>
#include <mutex>
#include <string>
namespace zone {
struct BuffPayload { int buff_id = 0; int stack = 0; uint64_t expire_ms = 0; };
struct BuffEvent { uint64_t player_id; int slot; uint32_t mask_after; };
struct TokenGrant { uint64_t request_id; uint32_t cost; uint32_t remain_after; };
class EventBus {
public:
void Publish(const BuffEvent& e) { std::lock_guard<std::mutex> g(m_); buff_q_.push_back(e); }
void PublishGrant(const TokenGrant& t) { std::lock_guard<std::mutex> g(m_); grant_q_.push_back(t); }
size_t BuffCount() const { std::lock_guard<std::mutex> g(m_); return buff_q_.size(); }
size_t GrantCount() const { std::lock_guard<std::mutex> g(m_); return grant_q_.size(); }
private:
mutable std::mutex m_;
std::deque<BuffEvent> buff_q_;
std::deque<TokenGrant> grant_q_;
};
class Metrics {
public:
void IncApplied() { applied_.fetch_add(1, std::memory_order_relaxed); }
void IncRejected() { rejected_.fetch_add(1, std::memory_order_relaxed); }
void IncGranted() { granted_.fetch_add(1, std::memory_order_relaxed); }
uint64_t applied() const { return applied_.load(std::memory_order_relaxed); }
uint64_t granted() const { return granted_.load(std::memory_order_relaxed); }
private:
std::atomic<uint64_t> applied_{0}, rejected_{0}, granted_{0};
};
struct PlayerBuffState {
uint64_t player_id = 0;
std::atomic<uint32_t> slot_mask{0};
BuffPayload payloads[32];
};
class BuffRegistry {
public:
bool TryApplyBuff(PlayerBuffState& st, int slot, const BuffPayload& p) {
if (slot < 0 || slot >= 32) return false;
const uint32_t bit = 1u << slot;
uint32_t expected = st.slot_mask.load(std::memory_order_relaxed);
if (expected & bit) {
metrics_.IncRejected();
return false;
}
const uint32_t desired = expected | bit;
st.payloads[slot] = p;
// (A)
if (!st.slot_mask.compare_exchange_weak(expected, desired,
std::memory_order_relaxed,
std::memory_order_relaxed)) {
metrics_.IncRejected();
return false;
}
metrics_.IncApplied();
bus_.Publish(BuffEvent{st.player_id, slot, desired});
return true;
}
bool TryAcquireSpawnToken(uint32_t cost) {
if (cost == 0) return false;
const uint32_t snapshot = tokens_.load(std::memory_order_relaxed);
uint32_t expected = snapshot;
for (;;) {
if (expected < cost) return false;
const uint32_t desired = expected - cost;
const uint64_t rid = next_request_id_.fetch_add(1, std::memory_order_relaxed);
metrics_.IncGranted(); // (B)
bus_.PublishGrant(TokenGrant{rid, cost, desired});
if (tokens_.compare_exchange_weak(expected, desired,
std::memory_order_relaxed,
std::memory_order_relaxed)) {
return true;
}
expected = snapshot; // (A)
}
}
void ReleaseSpawnToken(uint32_t cost) {
tokens_.fetch_add(cost, std::memory_order_relaxed);
}
bool ReadBuff(const PlayerBuffState& st, int slot, BuffPayload* out) const {
const uint32_t mask = st.slot_mask.load(std::memory_order_relaxed);
if (!(mask & (1u << slot))) return false;
*out = st.payloads[slot];
return true;
}
const Metrics& metrics() const { return metrics_; }
const EventBus& bus() const { return bus_; }
private:
std::atomic<uint32_t> tokens_{64};
std::atomic<uint64_t> next_request_id_{0};
Metrics metrics_;
EventBus bus_;
};
} // namespace zone 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.