10. 타이머 휠의 동시성: 비volatile cursor, 비동기 취소, 무보호 슬롯 리스트
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 타이머 휠(timing wheel)이다. 스킬 쿨다운, 버프 만료, 재접속
// 타임아웃 등 수십만 개의 타이머를 O(1) 로 굴리기 위한 자료구조다.
// - 휠은 N개의 슬롯(버킷)으로 구성. 전용 "tick 스레드"가 매 1ms 마다
// cursor 를 한 칸 전진시키고, 그 슬롯의 만료 타이머들을 실행한다.
// - 다수의 워커 스레드가 동시에 Schedule() 로 새 타이머를 임의 슬롯에 넣고,
// Cancel() 로 취소(논리 삭제) 한다.
// - "tick 은 단일 스레드라 슬롯 리스트는 안전하다"고 가정했다.
//
// 운영 중 증상(드물고 비결정적, 부하 의존):
// - 가끔 타이머가 "한 바퀴 늦게" 또는 "영영" 안 터진다.
// - 가끔 막 취소한 타이머의 콜백이 그래도 실행된다(취소 누락).
// - 아주 가끔 tick 스레드가 슬롯을 순회하다 예외(리스트 손상/중복)로 죽는다.
// - cursor 값을 워커가 읽어 "내 타이머가 곧 터지는지" 판단하는 로직이 틀린다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - Schedule/Cancel 은 여러 워커에서 동시에 안전해야 한다.
// - tick 스레드는 만료 슬롯을 정확히, 누락/중복 없이 실행해야 한다.
// - cursor 등 공유 상태는 다른 스레드에 정확한 값/순서로 보여야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 동시성 결함을 모두 찾고, atomic/메모리배리어 관점에서 정확히 설명하라.
// 2) (A)(B)(C)(D) 각 지점의 문제를 지적하라.
// 3) 정확하고 효율적으로 수정하라(설계 대안 포함).
// ============================================================================
using System;
namespace TimingWheelDemo
{
public sealed class TimerNode
{
public long ExpireTick;
public Action Callback;
public bool Canceled; // (B) 취소 플래그 (일반 bool)
public TimerNode Next;
}
public sealed class TimingWheel
{
private readonly int _slots;
private readonly TimerNode[] _buckets;
private long _cursor; // (A) tick 이 쓰고, 워커가 읽음 (일반 long)
public TimingWheel(int slots)
{
_slots = slots;
_buckets = new TimerNode[slots];
}
// 워커 스레드들이 호출 (동시)
public TimerNode Schedule(long delayTicks, Action cb)
{
long cur = _cursor; // (A) 현재 커서 읽기
long expire = cur + delayTicks;
int idx = (int)(expire % _slots);
var n = new TimerNode { ExpireTick = expire, Callback = cb, Canceled = false };
// (C) 슬롯 리스트 머리에 끼워넣기 (락 없음)
n.Next = _buckets[idx];
_buckets[idx] = n;
return n;
}
// 워커 스레드가 호출: 논리 취소
public void Cancel(TimerNode n)
{
// (B) 그냥 플래그만 세운다
n.Canceled = true;
}
// tick 스레드 1개만 호출 (1ms 마다)
public void Tick()
{
long cur = _cursor;
int idx = (int)(cur % _slots);
// 이 슬롯을 통째로 떼어내 순회
TimerNode node = _buckets[idx];
_buckets[idx] = null;
TimerNode keep = null;
while (node != null)
{
TimerNode nextNode = node.Next;
if (!node.Canceled && node.ExpireTick <= cur)
{
node.Callback();
// (D) 실행 후 참조 끊기 (논리 회수)
}
else if (node.Canceled)
{
// (D) 취소된 것도 버림
}
else
{
// 아직 만료 아님(다음 바퀴) → 보관
node.Next = keep;
keep = node;
}
node = nextNode;
}
// 보관 노드 되돌려놓기
_buckets[idx] = keep;
_cursor = cur + 1; // (A) 커서 전진
}
public long Cursor => _cursor; // (A) 워커가 읽음
}
} 결함 코드 · C++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 타이머 휠(timing wheel)이다. 스킬 쿨다운, 버프 만료, 재접속
// 타임아웃 등 수십만 개의 타이머를 O(1) 로 굴리기 위한 자료구조다.
// - 휠은 N개의 슬롯(버킷)으로 구성. 전용 "tick 스레드"가 매 1ms 마다
// cursor 를 한 칸 전진시키고, 그 슬롯의 만료 타이머들을 실행한다.
// - 다수의 워커 스레드가 동시에 Schedule() 로 새 타이머를 임의 슬롯에 넣고,
// Cancel() 로 취소(논리 삭제) 한다.
// - "tick 은 단일 스레드라 슬롯 리스트는 안전하다"고 가정했다.
//
// 운영 중 증상(드물고 비결정적, 부하 의존):
// - 가끔 타이머가 "한 바퀴 늦게" 또는 "영영" 안 터진다.
// - 가끔 막 취소한 타이머의 콜백이 그래도 실행된다(취소 누락).
// - 아주 가끔 tick 스레드가 슬롯을 순회하다 크래시(이미 해제/변형된 노드).
// - cursor 값을 워커가 읽어 "내 타이머가 곧 터지는지" 판단하는 로직이 틀린다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - Schedule/Cancel 은 여러 워커에서 동시에 안전해야 한다.
// - tick 스레드는 만료 슬롯을 정확히, 누락/중복 없이 실행해야 한다.
// - cursor 등 공유 상태는 다른 스레드에 정확한 값/순서로 보여야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 동시성 결함을 모두 찾고, atomic/메모리배리어 관점에서 정확히 설명하라.
// 2) (A)(B)(C)(D) 각 지점의 문제를 지적하라.
// 3) 정확하고 효율적으로 수정하라(설계 대안 포함).
// ============================================================================
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <functional>
#include <thread>
#include <vector>
struct TimerNode
{
std::uint64_t expireTick;
std::function<void()> callback;
bool canceled; // (B) 취소 플래그
TimerNode* next;
};
class TimingWheel
{
public:
explicit TimingWheel(int slots)
: slots_(slots), buckets_(slots, nullptr)
{
}
// 워커 스레드들이 호출 (동시)
void Schedule(std::uint64_t delayTicks, std::function<void()> cb)
{
std::uint64_t cur = cursor_; // (A) 현재 커서 읽기
std::uint64_t expire = cur + delayTicks;
int idx = static_cast<int>(expire % slots_);
TimerNode* n = new TimerNode{expire, std::move(cb), false, nullptr};
// (C) 슬롯 리스트 머리에 끼워넣기 (락 없음)
n->next = buckets_[idx];
buckets_[idx] = n;
}
// 워커 스레드가 호출: 논리 취소
void Cancel(TimerNode* n)
{
// (B) 그냥 플래그만 세운다
n->canceled = true;
}
// tick 스레드 1개만 호출 (1ms 마다)
void Tick()
{
std::uint64_t cur = cursor_;
int idx = static_cast<int>(cur % slots_);
// 이 슬롯을 통째로 떼어내 순회
TimerNode* node = buckets_[idx];
buckets_[idx] = nullptr;
TimerNode* keep = nullptr;
while (node)
{
TimerNode* nextNode = node->next;
if (!node->canceled && node->expireTick <= cur)
{
node->callback();
delete node; // (D) 실행 후 즉시 해제
}
else if (node->canceled)
{
delete node; // (D) 취소된 것도 해제
}
else
{
// 아직 만료 아님(다음 바퀴) → 보관
node->next = keep;
keep = node;
}
node = nextNode;
}
// 보관 노드 되돌려놓기
buckets_[idx] = keep;
cursor_ = cur + 1; // (A) 커서 전진
}
std::uint64_t Cursor() const { return cursor_; } // (A) 워커가 읽음
private:
int slots_;
std::vector<TimerNode*> buckets_;
std::uint64_t cursor_ = 0; // (A) tick 이 쓰고, 워커가 읽음
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.