Microsoft MVP성태의 닷넷 이야기
VC++: 103. C++ CreateTimerQueue, CreateTimerQueueTimer 예제 코드 [링크 복사], [링크+제목 복사],
조회: 20313
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 5개 있습니다.)
.NET Framework: 486. Java의 ScheduledExecutorService에 대응하는 C#의 System.Threading.Timer
; https://www.sysnet.pe.kr/2/0/1823

VC++: 103. C++ CreateTimerQueue, CreateTimerQueueTimer 예제 코드
; https://www.sysnet.pe.kr/2/0/11090

.NET Framework: 636. System.Threading.Timer를 이용해 타이머 작업을 할 때 유의할 점
; https://www.sysnet.pe.kr/2/0/11134

Windows: 189. WM_TIMER의 동작 방식 개요
; https://www.sysnet.pe.kr/2/0/12539

.NET Framework: 2019. C# - .NET에서 제공하는 3가지 Timer 비교
; https://www.sysnet.pe.kr/2/0/13069




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라는 이름이 들어 있는 걸로 봐서 맞는 것 같습니다. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 4/3/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2021-04-09 04시27분
[김종학] 안녕하세요 게시글 잘 보고 갑니다.

궁금한게 하나 있습니다.

CreateTimerQueueTimer호출 시 콜백 메서드를 전달할 때 클래스 멤버 메서드는 전달이 안되더군요. 외국 포럼에 가봐도 안된다는 얘기만 있는데, 정말 안되는게 맞나요??

클래스 메서드도 TimerQueue로 활용할 수 있다면 상당히 좋을 텐데요.
[guest]
2021-04-09 05시24분
@김종학 안 되는 게 맞습니다. 클래스 멤버 메서드는 C/C++ 코드 상으로는 숨겨져 있지만 반드시 this 인자를 받아 동작하게 되어 있습니다. 하지만 CreateTimerQueueTimer의 내부 구현에서 호출해야 할 콜백 메서드에 대해 어떤 this 인자를 넘겨야 할지 알 수 없으므로 그런 것이 불가능합니다. (사실, Win32 API는 C 함수이므로 C++에 종속적인 구현을 할 수 없습니다.) 그래서 설령 클래스 내에 콜백용의 함수를 정의해도 반드시 static 유형이어야 합니다.

즉, CreateTimerQueueTimer 뿐만 아니라 Win32 API에 있는 모든 콜백 함수가 지켜야 할 규칙입니다.
정성태
2021-04-09 06시49분
[김종학] 답변이 늦게 달릴 줄 알았는데 금방 달아주셨네요.

아무래도 원하는 기능은 직접 구현해야되겠네요 ㅜㅜ

답변 감사하고, 잘 배우고 가겠습니다.
[guest]
2023-04-03 09시18분
[Syong] 안녕하세요. 말씀해주신 대로 TimerQueue를 1회성이 아니라 다회성으로 사용하는게 맞을텐데요, 작성해주신 예제에서는 CreateTimerQueueTimer에 인자를 WT_EXECUTEONLYONCE로 전달하고, Sleep(1) 이후 계속해서 생성해서 발생시키고 있는데요, 이렇게 하면 Sleep(1)이 정확하게 1ms 보장이 되지 않지 않나요?
[guest]
2023-04-03 10시43분
@Syong "1ms 보장이 되지 않지 않나요?"의 답변은, 맞습니다. 보장이 되지 않고 시스템의 timer 설정에 따라 1ms 이상의 시간을 Sleep하게 됩니다.

윈도우 운영체제의 시간 함수 (2) - Sleep 함수의 동작 방식
; https://www.sysnet.pe.kr/2/0/11065
정성태
2023-04-03 10시54분
[Syong] TimerQueue와, MultimediaTimer 같은 기능이 정확한 주기로 같은 동작을 반복적으로 수행하는 것이 목적이 아니었나 싶기도하고, 해당 기능이 필요한 상황이라 여쭤봤었습니다.
[guest]
2023-04-03 11시19분
The Multimedia Timer for the .NET Framework
; https://www.codeproject.com/Articles/5501/The-Multimedia-Timer-for-the-NET-Framework

위의 Multimedia Timer 구현을 보면, Period Min 값이 1인 걸로 봐서 1ms 보장은 되는 듯합니다. 단지, "1ms가 되었어도 특정 동작이 끝나지 않았으면, 다음 주기에 동작 (중복실행방지)" 기능은 없으므로 그것은 별도로 처리를 해야 합니다. (일단, 여기서 답변이 되었으니, 질문 답변 게시판의 글은 삭제해도 될까요?)
정성태
2023-04-03 11시39분
[Syong] MultimediaTimer의 경우에는 1ms 보장이 되는 건 확인은 했는데, MultimediaTimer 같은 경우 운영체제에서 사용할 수 있는 총 수량 제한이 있는 것으로 알아서, TimerQueue로 동일 기능을 구현하려고 한 거였어서요 ㅠ TimerQueue는 용도가 다른 건가요?
[guest]
2023-04-03 11시46분
다르니까, 이름도 달라졌겠지요. ^^; 딱히 더 이상 제 지식에서는 언급할 것이 없습니다.
정성태

... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13188정성태12/9/20226231.NET Framework: 2075. C# - 직접 만들어 보는 TaskScheduler 실습 (SingleThreadTaskScheduler)파일 다운로드1
13187정성태12/8/20226147개발 환경 구성: 654. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법 (2)
13186정성태12/6/20224718오류 유형: 831. The framework 'Microsoft.AspNetCore.App', version '...' was not found.
13185정성태12/6/20225664개발 환경 구성: 653. Windows 환경에서의 Hello World x64 어셈블리 예제 (NASM 버전)
13184정성태12/5/20224877개발 환경 구성: 652. ml64.exe와 link.exe x64 실행 환경 구성
13183정성태12/4/20224798오류 유형: 830. MASM + CRT 함수를 사용하는 경우 발생하는 컴파일 오류 정리
13182정성태12/4/20225543Windows: 217. Windows 환경에서의 Hello World x64 어셈블리 예제 (MASM 버전)
13181정성태12/3/20224886Linux: 54. 리눅스/WSL - hello world 어셈블리 코드 x86/x64 (nasm)
13180정성태12/2/20225076.NET Framework: 2074. C# - 스택 메모리에 대한 여유 공간 확인하는 방법파일 다운로드1
13179정성태12/2/20224453Windows: 216. Windows 11 - 22H2 업데이트 이후 Terminal 대신 cmd 창이 뜨는 경우
13178정성태12/1/20225001Windows: 215. Win32 API 금지된 함수 - IsBadXxxPtr 유의 함수들이 안전하지 않은 이유파일 다운로드1
13177정성태11/30/20225710오류 유형: 829. uwsgi 설치 시 fatal error: Python.h: No such file or directory
13176정성태11/29/20224616오류 유형: 828. gunicorn - ModuleNotFoundError: No module named 'flask'
13175정성태11/29/20226293오류 유형: 827. Python - ImportError: cannot import name 'html5lib' from 'pip._vendor'
13174정성태11/28/20224809.NET Framework: 2073. C# - VMMap처럼 스택 메모리의 reserve/guard/commit 상태 출력파일 다운로드1
13173정성태11/27/20225542.NET Framework: 2072. 닷넷 응용 프로그램의 스레드 스택 크기 변경
13172정성태11/25/20225305.NET Framework: 2071. 닷넷에서 ESP/RSP 레지스터 값을 구하는 방법파일 다운로드1
13171정성태11/25/20224907Windows: 214. 윈도우 - 스레드 스택의 "red zone"
13170정성태11/24/20225187Windows: 213. 윈도우 - 싱글 스레드는 컨텍스트 스위칭이 없을까요?
13169정성태11/23/20225776Windows: 212. 윈도우의 Protected Process (Light) 보안 [1]파일 다운로드2
13168정성태11/22/20225093제니퍼 .NET: 31. 제니퍼 닷넷 적용 사례 (9) - DB 서비스에 부하가 걸렸다?!
13167정성태11/21/20225146.NET Framework: 2070. .NET 7 - Console.ReadKey와 리눅스의 터미널 타입
13166정성태11/20/20224900개발 환경 구성: 651. Windows 사용자 경험으로 WSL 환경에 dotnet 런타임/SDK 설치 방법
13165정성태11/18/20224794개발 환경 구성: 650. Azure - "scm" 프로세스와 엮인 서비스 모음
13164정성태11/18/20225707개발 환경 구성: 649. Azure - 비주얼 스튜디오를 이용한 AppService 원격 디버그 방법
13163정성태11/17/20225622개발 환경 구성: 648. 비주얼 스튜디오에서 안드로이드 기기 인식하는 방법
... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...