Microsoft MVP성태의 닷넷 이야기
VC++: 103. C++ CreateTimerQueue, CreateTimerQueueTimer 예제 코드 [링크 복사], [링크+제목 복사],
조회: 20177
글쓴 사람
정성태 (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분
다르니까, 이름도 달라졌겠지요. ^^; 딱히 더 이상 제 지식에서는 언급할 것이 없습니다.
정성태

... 46  47  48  49  50  51  52  53  [54]  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12278정성태7/23/20209468VS.NET IDE: 149. ("Binary was not built with debug information" 상태로) 소스 코드 디버깅이 안되는 경우
12277정성태7/23/202010915개발 환경 구성: 498. DEVPATH 환경 변수의 사용 예 - .NET Reflector의 (PDB 연결이 없는) DLL의 소스 코드 디버깅
12276정성태7/23/202010126.NET Framework: 930. 개발자를 위한 닷넷 어셈블리 바인딩 - DEVPATH 환경 변수
12275정성태7/22/202012662개발 환경 구성: 497. 닷넷에서 접근해보는 InterSystems의 IRIS Data Platform 데이터베이스파일 다운로드1
12274정성태7/21/202012056개발 환경 구성: 496. Azure - Blob Storage Account의 Location 이전 방법 [1]파일 다운로드1
12273정성태7/18/202013683개발 환경 구성: 495. Azure - Location이 다른 웹/DB 서버의 경우 발생하는 성능 하락
12272정성태7/16/20208656.NET Framework: 929. (StrongName의 버전 구분이 필요 없는) .NET Core 어셈블리 바인딩 규칙 [2]파일 다운로드1
12271정성태7/16/202010717.NET Framework: 928. .NET Framework의 Strong-named 어셈블리 바인딩 (2) - 런타임에 바인딩 리디렉션파일 다운로드1
12270정성태7/16/202011545오류 유형: 633. SSL_CTX_use_certificate_file - error:140AB18F:SSL routines:SSL_CTX_use_certificate:ee key too small
12269정성태7/16/20208555오류 유형: 632. .NET Core 웹 응용 프로그램 - The process was terminated due to an unhandled exception.
12268정성태7/15/202010709오류 유형: 631. .NET Core 웹 응용 프로그램 오류 - HTTP Error 500.35 - ANCM Multiple In-Process Applications in same Process
12267정성태7/15/202012380.NET Framework: 927. C# - 윈도우 프로그램에서 Credential Manager를 이용한 보안 정보 저장파일 다운로드1
12266정성태7/14/202010087오류 유형: 630. 사용자 계정을 지정해 CreateService API로 서비스를 등록한 경우 "Error 1069: The service did not start due to a logon failure." 오류발생
12265정성태7/10/20209236오류 유형: 629. Visual Studio - 웹 애플리케이션 실행 시 "Unable to connect to web server 'IIS Express'." 오류 발생
12264정성태7/9/202018304오류 유형: 628. docker: Error response from daemon: Conflict. The container name "..." is already in use by container "...".
12261정성태7/9/202011224VS.NET IDE: 148. 윈도우 10에서 .NET Core 응용 프로그램을 리눅스 환경에서 실행하는 2가지 방법 - docker, WSL 2 [5]
12260정성태7/8/20209625.NET Framework: 926. C# - ETW를 이용한 ThreadPool 스레드 감시파일 다운로드1
12259정성태7/8/20209168오류 유형: 627. nvlddmkm.sys의 BAD_POOL_HEADER BSOD 문제 [1]
12258정성태7/8/202012323기타: 77. DataDog APM 간략 소개
12257정성태7/7/20209342.NET Framework: 925. C# - ETW를 이용한 Monitor Enter/Exit 감시파일 다운로드1
12256정성태7/7/20209754.NET Framework: 924. C# - Reflection으로 변경할 수 없는 readonly 정적 필드 [4]
12255정성태7/6/202010182.NET Framework: 923. C# - ETW(Event Tracing for Windows)를 이용한 Finalizer 실행 감시파일 다운로드1
12254정성태7/2/202010033오류 유형: 626. git - REMOTE HOST IDENTIFICATION HAS CHANGED!
12253정성태7/2/202011096.NET Framework: 922. C# - .NET ThreadPool의 Local/Global Queue파일 다운로드1
12252정성태7/2/202013068.NET Framework: 921. C# - I/O 스레드를 사용한 비동기 소켓 서버/클라이언트파일 다운로드2
12251정성태7/1/202011040.NET Framework: 920. C# - 파일의 비동기 처리 유무에 따른 스레드 상황 [1]파일 다운로드2
... 46  47  48  49  50  51  52  53  [54]  55  56  57  58  59  60  ...