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

C++ CreateTimerQueue, CreateTimerQueueTimer 예제 코드

CreateTimerQueue, CreateTimerQueueTimer Win32 API 함수가 있습니다.

CreateTimerQueue function
; https://docs.microsoft.com/en-us/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-createtimerqueue

CreateTimerQueueTimer function
; https://docs.microsoft.com/en-us/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-createtimerqueuetimer

예제 코드는 다음에서 구할 수 있는데,

Using Timer Queues
; https://docs.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라는 이름이 들어 있는 걸로 봐서 맞는 것 같습니다. ^^




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

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 10/19/2019 ]

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

비밀번호

댓글 쓴 사람
 



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

궁금한게 하나 있습니다.

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

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

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

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

답변 감사하고, 잘 배우고 가겠습니다.
[손님]

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12631정성태5/5/202141사물인터넷: 60. ThingSpeak 사물인터넷 플랫폼에 ESP8266 NodeMcu + 조도 센서 장비 연동파일 다운로드1
12630정성태5/5/202125사물인터넷: 59. NodeMCU ESP8266 보드의 A0 핀 사용법 - Cds Cell(GL3526) 조도 센서 연동파일 다운로드1
12629정성태5/5/202173.NET Framework: 1057. C# - CoAP 서버 및 클라이언트 제작 (UDP 소켓 통신)파일 다운로드1
12628정성태5/4/20219Linux: 39. Eclipse 원격 디버깅 - Cannot run program "gdb": Launching failed
12627정성태5/4/202119Linux: 38. 라즈베리 파이 제로 용 프로그램 개발을 위한 Eclipse C/C++ 윈도우 환경 설정
12626정성태5/3/202137.NET Framework: 1056. C# - Thread.Suspend 호출 시 응용 프로그램 hang 현상 (2)파일 다운로드1
12625정성태5/3/202119오류 유형: 714. error CS5001: Program does not contain a static 'Main' method suitable for an entry point
12624정성태5/3/2021179.NET Framework: 1055. C# - struct/class가 스택/힙에 할당되는 사례 정리 [6]파일 다운로드1
12623정성태5/2/202177.NET Framework: 1054. C# 9 최상위 문에 STAThread 사용파일 다운로드1
12622정성태5/3/202115오류 유형: 713. XSD 파일을 포함한 프로젝트 - The type or namespace name 'TypedTableBase<>' does not exist in the namespace 'System.Data'
12621정성태5/1/2021100.NET Framework: 1053. C# - 특정 레지스트리 변경 시 알림을 받는 방법파일 다운로드1
12620정성태5/3/2021304.NET Framework: 1052. C# - 왜 구조체는 16 바이트의 크기가 적합한가? [1]파일 다운로드1
12619정성태4/29/2021244.NET Framework: 1051. C# - 구조체의 크기가 16바이트가 넘어가면 힙에 할당된다? [1]파일 다운로드1
12618정성태5/5/202189사물인터넷: 58. NodeMCU ESP8266 CP2102 Module을 이용한 WiFi UDP 통신파일 다운로드1
12617정성태4/26/202159.NET Framework: 1050. C# - ETW EventListener의 Keywords 별 EventId에 따른 필터링 방법파일 다운로드1
12616정성태4/26/2021117.NET Framework: 1049. C# - ETW EventListener를 상속받았을 때 초기화 순서파일 다운로드1
12615정성태4/26/202141오류 유형: 712. Microsoft Live 로그인 - 계정을 선택하는(Pick an account) 화면에서 진행이 안 되는 문제
12614정성태4/24/202199개발 환경 구성: 570. C# - Azure AD 인증을 지원하는 ASP.NET Core 5.0 웹 애플리케이션 예제 구성파일 다운로드1
12613정성태4/23/202190.NET Framework: 1048. C# - ETW 이벤트의 Keywords에 속한 EventId 구하는 방법 (2) 관리 코드파일 다운로드1
12612정성태4/23/2021122.NET Framework: 1047. C# - ETW 이벤트의 Keywords에 속한 EventId 구하는 방법 (1) PInvoke파일 다운로드1
12611정성태4/22/2021135오류 유형: 711. 닷넷 EXE 실행 오류 - Mixed mode assembly is build against version 'v2.0.50727' of the runtime
12610정성태4/22/2021156.NET Framework: 1046. C# - 컴파일 시점에 참조할 수 없는 타입을 포함한 이벤트 핸들러를 Reflection을 이용해 구독하는 방법파일 다운로드1
12609정성태4/22/2021133.NET Framework: 1045. C# - 런타임 시점에 이벤트 핸들러를 만들어 Reflection을 이용해 구독하는 방법파일 다운로드1
12608정성태4/22/2021318.NET Framework: 1044. C# - Generic Host를 이용해 .NET 5로 리눅스 daemon 프로그램 만드는 방법 [6]파일 다운로드1
12607정성태4/21/2021136.NET Framework: 1043. C# - 실행 시점에 동적으로 Delegate 타입을 만드는 방법파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...