C++ CreateTimerQueue, CreateTimerQueueTimer 예제 코드
CreateTimerQueue, CreateTimerQueueTimer Win32 API 함수가 있습니다.
CreateTimerQueue function
; https://learn.microsoft.com/en-us/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-createtimerqueue
CreateTimerQueueTimer function
; https://learn.microsoft.com/en-us/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-createtimerqueuetimer
예제 코드는 다음에서 구할 수 있는데,
Using Timer Queues
; https://learn.microsoft.com/en-us/windows/win32/sync/using-timer-queues
만족스럽지 않습니다. TimerQueue 생성하고, Timer 작업 하나 설정한 다음 곧바로 정리하는 것이 그다지 현실적인 예제로 보이지 않습니다.
실 사용 사례로 보면, 응용 프로그램 시작하면서 TimerQueue를 하나 생성해 둘 것입니다. 그 TimerQueue는 응용 프로그램이 끝나는 시점에 닫을 것이고, 응용 프로그램이 동작하는 중에는 끊임없이 시간 관련한 작업을 TimerQueue에 던지게 될 것입니다. 이를 간단하게 코딩으로 옮겨보면 다음과 같은 방식이 됩니다.
#include "stdafx.h"
#include <Windows.h>
using namespace std;
HANDLE g_hTimerQueue;
VOID CALLBACK timerCallback(PVOID lpParam, BOOLEAN TimerOrWaitFired)
{
// ...
}
int main()
{
g_hTimerQueue = CreateTimerQueue();
HANDLE hCleanTimer = nullptr;
while (true)
{
HANDLE hTimer;
BOOL result = CreateTimerQueueTimer(&hTimer, g_hTimerQueue, timerCallback, nullptr, 100, 0, WT_EXECUTEONLYONCE);
Sleep(20);
}
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE);
return 0;
}
그런데, 위와 같이 하면 메모리 누수(Memory leaks)가 발생합니다. 왜냐하면 CreateTimerQueueTimer로 TimerQueue에 추가한 작업은 반드시 DeleteTimerQueueTimer를 이용해 해제해야 하기 때문입니다.
(개인적으로) 해제하기 가장 좋은 장소는 바로 timerCallback이며, 따라서 다음과 같이 작업해 주면 됩니다.
// ...[생략]...
typedef struct TimerWorkItem
{
public:
int workIndex = 0;
HANDLE handle = nullptr;
};
VOID CALLBACK timerCallback(PVOID lpParam, BOOLEAN TimerOrWaitFired)
{
TimerWorkItem *workItem = (TimerWorkItem *)lpParam;
if (workItem == nullptr)
{
return;
}
printf("%d\n", workItem->workIndex);
::DeleteTimerQueueTimer(g_hTimerQueue, workItem->handle, nullptr);
delete workItem;
}
int main()
{
g_hTimerQueue = CreateTimerQueue();
int workIndex = 0;
while (true)
{
TimerWorkItem *workItem = new TimerWorkItem();
workItem->workIndex = workIndex++;
BOOL result = CreateTimerQueueTimer(&workItem->handle, g_hTimerQueue, timerCallback, workItem, 100, 0, WT_EXECUTEONLYONCE);
Sleep(1);
}
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE);
return 0;
}
참고로, CreateTimerQueueTimer의 handle 반환값을 다음과 같이 처리해 보기도 했었습니다.
// ...[생략]...
VOID CALLBACK timerCallback(PVOID lpParam, BOOLEAN TimerOrWaitFired)
{
TimerWorkItem *workItem = (TimerWorkItem *)lpParam;
if (workItem == nullptr)
{
return;
}
HANDLE hTimer = workItem->handle;
if (hTimer == nullptr)
{
return;
}
printf("%d\n", workItem->workIndex);
::DeleteTimerQueueTimer(g_hTimerQueue, hTimer, nullptr);
delete workItem;
}
int main()
{
int workIndex = 0;
g_hTimerQueue = CreateTimerQueue();
while (true)
{
HANDLE hTimer;
TimerWorkItem *workItem = new TimerWorkItem();
workItem->workIndex = workIndex++;
BOOL result = CreateTimerQueueTimer(&hTimer, g_hTimerQueue, timerCallback, workItem, 100, 0, WT_EXECUTEONLYONCE);
if (result == TRUE)
{
workItem->handle = hTimer;
}
Sleep(1);
}
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE);
return 0;
}
위와 같이 하면, CreateTimerQueueTimer 이후에 timer handle을 workItem에 보관하게 되는데 시간 차이가 발생한다고는 해도 nullptr을 조사하는 노력이 무색하게 DeleteTimerQueueTimer에서 다음과 같은 식의 예외가 가끔씩 발생합니다.
Unhandled exception at 0x774E5251 (ntdll.dll) in ConsoleApplication1.exe: A LIST_ENTRY has been corrupted (i.e. double remove).
Exception thrown at 0x774E517B (ntdll.dll) in ConsoleApplication1.exe: 0xC0000005: Access violation reading location 0xDDDDDDF9.
If there is a handler for this exception, the program may be safely continued.
빈번하게 TimerQueue를 생성/삭제하는 경우라면 DeleteTimerQueueEx를 할 때도 주의를 기울여야 합니다. 실제로 다음과 같은 코드를 테스트해보면,
// ...[생략]...
int main()
{
int workIndex = 0;
while (true)
{
workIndex = 0;
g_hTimerQueue = CreateTimerQueue();
while (workIndex < 1500)
{
TimerWorkItem *workItem = new TimerWorkItem();
workItem->workIndex = workIndex++;
BOOL result = CreateTimerQueueTimer(&workItem->handle, g_hTimerQueue, timerCallback, workItem, 100, 0, WT_EXECUTEONLYONCE);
Sleep(1);
}
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE);
}
return 0;
}
DeleteTimerQueueEx에서 다음과 같은 식의 예외가 잦은 빈도로 발생합니다.
Unhandled exception at 0x7759C2C8 (ntdll.dll) in ConsoleApplication1.exe: 0xC000000D: An invalid parameter was passed to a service or function.
예상으로는, Timer Queue에 현재 처리 중인 작업이 있어서 (
하지만 문서에서는 INVALID_HANDLE_VALUE로 호출하면 모든 콜백 작업이 완료되기를 기다린다고 되어 있습니다.) 그것과 충돌이 발생하는 듯합니다.
위의 예제 같은 경우에는 DeleteTimerQueueEx 호출 전 (모든 콜백이 완료될) 1초 정도의 Sleep을 주면 아무런 오류 없이 잘 실행됩니다.
// ...[생략]...
int main()
{
int workIndex = 0;
while (true)
{
workIndex = 0;
g_hTimerQueue = CreateTimerQueue();
while (workIndex < 1500)
{
TimerWorkItem *workItem = new TimerWorkItem();
workItem->workIndex = workIndex++;
BOOL result = CreateTimerQueueTimer(&workItem->handle, g_hTimerQueue, timerCallback, workItem, 100, 0, WT_EXECUTEONLYONCE);
Sleep(1);
}
Sleep(1000);
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE);
}
return 0;
}
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
예전에 CreateTimerQueueTimer가 왠지 System.Threading.Timer에 사용되었을 것 같다는 이야기를 했었는데요.
Java의 ScheduledExecutorService에 대응하는 C#의 System.Threading.Timer
; https://www.sysnet.pe.kr/2/0/1823
이참에 콜 스택을 좀 확인해 보니,
ConsoleApplication2.exe!ConsoleApplication2.Program.timerFunc(object state) Line 21 C#
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown
mscorlib.dll!System.Threading.TimerQueueTimer.CallCallback() Unknown
mscorlib.dll!System.Threading.TimerQueueTimer.Fire() Unknown
mscorlib.dll!System.Threading.TimerQueue.FireNextTimers() Unknown
[Native to Managed Transition]
kernel32.dll!BaseThreadInitThunk() Unknown
ntdll.dll!RtlUserThreadStart() Unknown
mscorlib.dll 내에 대놓고 TimerQueueTimer라는 이름이 들어 있는 걸로 봐서 맞는 것 같습니다. ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]