6.Timer Queue (Thread pool에서 tick event를 수행하는 모듈)

Windows XP, 2000 이후 버전의 운영체제에서는 Windows Timer Queue라는 API를 제공하고 있습니다. ProudNet은 이것을 쉽게 쓸 수 있도록 하는 API를 제공하고 있습니다.

Timer Queue는 일정 시간마다 사용자가 지정한 함수를 실행시키되, 그 함수는 스레드 풀(Thread pool)의 스레드 중 하나에서 실행됩니다. 만약 모든 스레드가 뭔가를 실행중인 경우(즉 running state) 함수의 실행은 스레드 중 과거 작업이 완료되는 스레드가 등장할 때까지 보류됩니다.

Timer queue에서 호출하는 유저 함수는 스레드 풀에 있는 스레드 중 하나가 선택되는데, 만약 앞서 실행중이던 유저 함수가 실행이 완료되지 않은 상태이더라도 할 일이 없는 스레드(idle state)가 있는 경우 그 스레드가 선택되어서 유저 함수를 실행합니다.

다음과 같은 작업 리스트가 있다고 가정합시다. 검은 화살표는 0.1초이며 A,B,C,D,E는 매 0.1초마다 해야 할 작업 항목입니다. A,D는 0.1초 이내에 끝나며, B는 딱 0.1초에 끝나며, C,E는 0.1초 안에 끝나지 못하는 작업입니다.

그림 6-1일정 시간마다 실행해야 하는 작업 항목들

10. 서버 메인 루프의 이해 방식인 경우 이들 작업 항목은 다음 그림처럼 수행됩니다. 한 개의 스레드에서 모든 작업 항목을 실행하기 때문에 D,E는 제때 시작하지 못합니다.

그림 6-2한 개의 스레드에서 작업 항목들을 실행하는 경우

그러나 타이머 큐 방식에서는 D를 실행하기 위해 또 다른 스레드가 동원되며 E는 앞서 C를 완료한 스레드에서 제때에 실행되고 있습니다.

그림 6-3타이머 큐 방식에서 작업 항목들을 실행하는 경우

즉 타이머 큐 방식은 제때에 필요한 작업을 실행하되 필요한 경우 스레드를 더 동원한다는 특징이 있습니다.

타이머 큐는 유저 함수가 동시에 두 개 이상의 스레드에서 실행되고 있을 수도 있음을 의미합니다. 이러한 특징 때문에 Timer queue는 주로 서버 프로그램에서 사용됩니다. Timer queue는 병렬성을 가능하게 해주는 데신 병렬성이 가지고 있는 위험성을 감수해야 합니다. 따라서 사용 전에 timer queue의 병렬성이 필요한지를 판단하시기를 권고합니다.

(주의: 이러한 판단 없이 타이머 큐를 잘못 사용할 경우 서버 프로세스의 스레드가 폭발적으로 증가해서 성능에 역효과를 줄 수 있습니다! 타이머 큐를 꼭 써야 하는 이유가 없다면 Proud.CTimerThread16. 서버에서 타이머 루프, RMI, 이벤트 처리하기을 쓰는 것이 편리합니다.)

Timer queue를 쓰려면 Proud.CTimerQueue클래스를 접근해야 합니다. 이 클래스는 singleton입니다. 일정 시간마다 호출될 함수와 호출 주기를 Proud::NewTimerParam 구조체에 설정하여 Proud.CTimerQueue.NewTimer 에 인자로 넣어주면 Proud.CTimerQueueTimer 객체를 받게 됩니다. 그리고 Proud.CTimerQueueTimer 객체를 파괴하기 전까지는 지정한 유저 함수가 일정 시간마다 실행됩니다.

VOID NTAPI UserFunction(void* context, BOOLEAN TimerOrWaitFired)
{
    int *pCallCount = static_cast<int *>(context);

    // 유저 함수
    std::cout << "UserFunction : " << ++(*pCallCount) << std::endl;
}

int _tmain(int argc, TCHAR* argv[])
{
    // NewTimerParam 구조체 변수 선언.
    NewTimerParam p1;

    // 테스트로 카운팅에 쓰일 변수 선언
    int callCount = 0;

    // 유저 함수 설정.
    p1.m_callback = UserFunction;

    // 매개변수로 받을 포인터 설정.
    p1.m_pCtx = &callCount;

    // 1초 이후부터 콜백이 시작되도록 설정.
    p1.m_DueTime = 1000;

    // 0.1초 간격으로 콜백 되도록 설정.
    p1.m_period = 100;

    // 일정 시간마다 스레드 풀에서 유저 함수가 0.1초마다 호출되도록 한다.
    Proud::CTimerQueueTimer* ret = Proud::CTimerQueue::GetSharedPtr()->NewTimer(p1);

    std::cout << "PRESS ANY KEY TO EXIT" << std::endl;

    // 유저 콜백이 호출될 때까지 대기
    _getch();

    // 타이머 객체를 파괴한다. 파괴한 후에는 유저 함수가 더 이상 호출되지 않는다.
    delete ret;
}