카테고리 없음

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