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

... 151  152  153  154  155  156  157  158  [159]  160  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1073정성태6/20/201127148오류 유형: 127. Visual Studio에서 WCF 서비스의 이름 변경 시 발생할 수 있는 오류
1072정성태6/19/201126607.NET Framework: 224. EF 4.1 Code First에서 Identity 칼럼 생성하는 방법파일 다운로드1
1071정성태6/19/201130150.NET Framework: 223. Entity Framework 4.1의 Code First를 이용한 SQL Azure 데이터베이스 생성 [3]파일 다운로드1
1070정성태6/19/201127676.NET Framework: 222. Windows Azure - VM Role 베타 프로그램 참여 [2]
1069정성태6/18/201127761.NET Framework: 221. Cache 영향을 받지 않는 DNS 이름 풀이 [2]파일 다운로드1
1068정성태6/16/201125368개발 환경 구성: 127. Portable Library - 닷넷 N-Screen용 공통 라이브러리 제작 [1]
1067정성태6/15/201124935오류 유형: 126. Windows failed to apply the Group Policy Folder Options settings. [1]
1066정성태6/14/201127944개발 환경 구성: 126. MSDN 구독자 - Windows Azure 무료 서비스 신청하는 방법 [4]
1065정성태6/13/201132771개발 환경 구성: 125. Firebird - 유니코드 기본 문자셋 지정
1064정성태6/11/201127429웹: 22. Visual Studio 2010에서 CSS 3 인텔리센스(intellisense) 지원하는 방법 [1]
1063정성태6/10/201129012웹: 21. Sysnet 웹 사이트의 CSS 2.1 변환 기록 [1]
1062정성태6/9/201129187웹: 20. Sysnet 웹 사이트의 HTML5 변환 기록 [1]
1061정성태6/8/201127430오류 유형: 125. 인터넷 익스플로러 - 개발자 도구에서 정지점(BP: Breakpoint) 설정이 안 되는 경우 [1]
1060정성태6/8/201123998VC++: 51. PHP 모듈의 F5 디버깅
1059정성태6/6/201129116VC++: 50. PHP 모듈 - php_mysql 빌드하는 방법파일 다운로드1
1058정성태6/5/201132750개발 환경 구성: 124. .NET 개발자가 처음 해보는 PHP + MySQL 연동 [2]
1057정성태6/4/201130134VC++: 49. 소스 코드로부터 php5apache2_2.dll 생성하는 방법파일 다운로드1
1056정성태6/2/201128290VC++: 48. 윈도우에서 Apache Module - Content Handler 컴파일파일 다운로드1
1055정성태6/1/201125516오류 유형: 124. MVC 프로젝트의 Site.Master 관련 오류 정리
1054정성태5/31/201129751.NET Framework: 220. ASP.NET MVC Web Site 프로젝트 - 단위 테스트 작성파일 다운로드1
1053정성태5/31/201132300VC++: 47. Apache Module에 대한 'F5 디버그 (Start with debugging)' [2]
1052정성태5/30/201129928.NET Framework: 219. ASP.NET MVC Web Site 프로젝트 구성하기파일 다운로드1
1051정성태5/28/201138410VC++: 46. 윈도우에서 Apache Module 컴파일 (VC++)파일 다운로드1
1050정성태5/28/201124599오류 유형: 123. Firebird - Exception of type 'FirebirdSql.Data.Common.IscException' was thrown.
1049정성태5/28/201130280.NET Framework: 218. WCF REST 서비스 - 웹 브라우저 측 Ajax 호출 캐시 [1]
1048정성태5/27/201132213개발 환경 구성: 123. Apache 소스를 윈도우 환경에서 빌드하기
... 151  152  153  154  155  156  157  158  [159]  160  161  162  163  164  165  ...