카테고리 없음
Time Wheel 3
denny
2025. 4. 8. 15:41
진짜 중요한 포인트!
Time Wheel을 구현할 때 **슬롯 개수(slot count)**를 고정할지 동적으로 늘릴지에 따라
성능/복잡도/유연성이 크게 달라져. 아래에 차이점과 구현 팁 정리해볼게.
✅ 1. 고정형 Time Wheel (Fixed Size)
📌 개요
- 슬롯 수(slotCount)가 미리 정해짐 (예: 128, 256, 1024)
- tick() 함수에서 인덱스는 순환 (0 → 1 → ... → slotCount - 1 → 0)
- 대부분의 MMO 서버에서 선호됨 (성능 안정성 + 단순 구조)
🎯 특징
항목 설명
속도 | 매우 빠름 (슬롯 인덱스를 & 연산으로 최적화 가능) |
구현 난이도 | 쉬움 |
메모리 사용 | 예측 가능, 고정 |
확장성 | delay가 너무 클 경우 제한 존재 (ex: 5초까지만 스케줄링) |
응용 | 게임 서버, 일정한 주기로 반복되는 작업들에 적합 |
📌 제한 보완 (회전수 방식)
- 오브젝트에 **“회전수(rounds)”**를 추가로 설정해서
delay > slotCount * tickInterval인 경우도 처리 가능
struct TimerTask {
GameObject* obj;
int rounds; // 이만큼 더 회전 후 실행
};
void Tick() {
auto& bucket = timeWheel[currentSlot];
for (auto it = bucket.begin(); it != bucket.end();) {
if (it->rounds > 0) {
--(it->rounds);
++it;
} else {
it->obj->SendSnapshot();
Schedule(it->obj, it->obj->GetUpdateDelay());
it = bucket.erase(it);
}
}
currentSlot = (currentSlot + 1) & SLOT_MASK; // or if/else 방식
}
✅ 2. 동적 Time Wheel (Dynamic / Hierarchical)
📌 개요
- 슬롯 수를 동적으로 늘리거나, 또는 **여러 계층(Hierarchy)**을 둠
- 너무 먼 미래에 실행되는 작업은 상위 타임휠로 올려보냄
- 대표적 예시: Hierarchical Timing Wheel (HTW)
🎯 구조 예시 (2계층)
레벨 슬롯 수 틱 간격
레벨 0 | 256 | 10ms |
레벨 1 | 64 | 256 * 10ms = 2560ms (~2.5초) |
- 어떤 작업이 100ms 후라면 → L0에 등록
- 어떤 작업이 10초 후라면 → L1에 등록 → 시간이 가까워지면 L0로 내려보냄
📌 장단점
항목 장점 단점
확장성 | 사실상 무제한 타이머 등록 가능 | 구조가 복잡 |
메모리 사용 | 지연이 크면 슬럿 수 적게 쓸 수 있음 | 구현 시 조심해야 할 것 많음 |
응용 | OS 커널 타이머, 정밀한 타이머 스케줄러에 적합 | 게임 서버에서는 너무 복잡할 수 있음 |
Netty의 HashedWheelTimer나 Linux의 timerfd 같은 것들이 이런 구조를 사용함
🎯 어떤 걸 선택해야 할까?
상황 추천 방식
게임 서버에서 반복적인 스냅샷, 버프 갱신 등 | ✅ 고정형 Time Wheel + 회전수 |
타이머가 아주 다양하고, 복잡한 스케줄링 필요 | ✅ 동적/계층형 구조 |
네트워크/서버에서 몇 초~수십 분 후 타이머 필요 | ✅ Hierarchical 방식 (예: 서버 세션 타임아웃) |
💡 정리
항목 고정형 동적/계층형
구현 난이도 | 쉬움 | 어려움 |
처리 성능 | 빠름 | 약간 느릴 수 있음 |
정밀도 | 틱 간격 제한 | 계층 간격에 따라 달라짐 |
유스케이스 | 게임 서버, 반복 로직 | 범용 스케줄러, 네트워크, 시스템 타이머 |
🧩 Hierarchical Timing Wheel 전체 코드 예시 (C++ 스타일)
다음은 **2계층 타임 휠 구조 (Level 0, Level 1)**를 간단하게 구현한 코드 예시야.
Tick마다 Level 1에서 Level 0으로 작업을 다운시키고, Level 0에서는 바로 실행돼.
✅ 구조 상수 및 데이터 타입
const int LEVEL0_SLOTS = 256;
const int LEVEL1_SLOTS = 64;
const int TICK_MS = 10;
int level0_current = 0;
int level1_current = 0;
struct Task {
GameObject* obj;
int delayMs;
};
✅ TimeWheel 클래스
class TimeWheel {
public:
std::vector<std::list<Task>> level0;
std::vector<std::list<Task>> level1;
TimeWheel() {
level0.resize(LEVEL0_SLOTS);
level1.resize(LEVEL1_SLOTS);
}
void schedule(GameObject* obj, int delayMs) {
int ticks = delayMs / TICK_MS;
if (ticks < LEVEL0_SLOTS) {
int slot = (level0_current + ticks) % LEVEL0_SLOTS;
level0[slot].push_back({obj, delayMs});
} else {
int slot = (level1_current + (ticks / LEVEL0_SLOTS)) % LEVEL1_SLOTS;
level1[slot].push_back({obj, delayMs});
}
}
void tick() {
// 1. Level0 슬롯 실행
auto& slot0 = level0[level0_current];
for (auto& task : slot0) {
task.obj->SendSnapshot();
schedule(task.obj, task.obj->GetUpdateDelay());
}
slot0.clear();
// 2. 1바퀴 돌면 Level1 → Level0로 작업 이동
if (level0_current == 0) {
auto& slot1 = level1[level1_current];
for (auto& task : slot1) {
schedule(task.obj, task.delayMs - LEVEL0_SLOTS * TICK_MS);
}
slot1.clear();
level1_current = (level1_current + 1) % LEVEL1_SLOTS;
}
// 3. 인덱스 증가
level0_current = (level0_current + 1) % LEVEL0_SLOTS;
}
};
✅ 사용 예시
TimeWheel timer;
void gameLoop() {
timer.tick(); // 매 프레임 또는 일정 주기로 호출
// 기타 게임 처리 로직
}
void registerObject(GameObject* obj) {
timer.schedule(obj, obj->GetUpdateDelay()); // 처음 등록
}
✅ 추가 기능 예시 (선택적으로 구현)
기능 구현 방식
타이머 취소 | 오브젝트 ID를 map에 등록해서 찾기 |
타이머 변경 | remove 후 새 delay로 재등록 |
반복 X 단발 타이머 | SendSnapshot() 후 재등록 생략 |
Level 2 추가 | LEVEL1_SLOTS 초과 시 한 단계 더 위로 push |