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

... 181  182  183  184  185  186  187  188  189  190  191  192  193  [194]  195  ...
NoWriterDateCnt.TitleFile(s)
91정성태11/14/200518719VC++: 12. VS.NET 2005 VC++ Debug: Expression: ( (state != ST_INVALID ) )
90정성태1/27/200519540.NET Framework: 22. Debug: The underlying connection was closed: Unable to connect to the remote server.
89정성태1/26/200524008VC++: 11. Delay Loaded DLL
87정성태1/23/200517610VS.NET IDE: 18. VS.NET 2005 Beta 1 - VC++ 프로젝트에서 Connection Point 구현시 버그
88정성태1/23/200517318    답변글 VS.NET IDE: 18.1. VS.NET 2003 : VC++ 프로젝트에서 Connection Point 추가시에도 버그
86정성태1/23/200523031.NET Framework: 21. Code Snippet - Enum과 관련된 다양한 형변환 [1]
85정성태1/23/200521206스크립트: 4. Windows 2003에서 BHO(Browser Helper Objects) 동작 안하는 현상 [1]
83정성태1/18/200526351.NET Framework: 20. System.AccessViolationException 예외가 발생한 한 예.
82정성태1/3/200519817VS.NET IDE: 17. Windows 운영 - 특정 사용자 또는 그룹에 대해서 파일 공유 접근 금지
79정성태1/20/200527764기타: 8. DELL Latitude D800 노트북 컴퓨터의 PC Beep 소음(!) 문제.
78정성태12/27/200420112VS.NET IDE: 16. MS 제품 관련 사용되는 TCP/IP 포트 열거파일 다운로드1
77정성태12/27/200420364VS.NET IDE: 15. Virtual CD-ROM Control Panel - ISO 이미지를 CD-ROM 드라이브처럼 접근하게 해주는 EXE 프로그램 [1]파일 다운로드1
76정성태12/27/200421444VS.NET IDE: 14. VPN 접속시 IP를 고정적으로 할당받는 방법 [1]
75정성태12/27/200417662VS.NET IDE: 13. VS.NET 2005 Beta 1 - Portfolio Explorer 에 등록된 Team Server 항목 삭제 방법
84정성태1/19/200518562    답변글 VS.NET IDE: 13.1. VS.NET 2005 Beta 1 : Team Server 에 등록된 포트폴리오 프로젝트 삭제 방법
74정성태12/26/200419158VS.NET IDE: 12. [시나리오] VS.NET 2005 Team Foundation Server을 Virtual Server에 설치 [1]
80정성태12/31/200418472    답변글 VS.NET IDE: 12.1. Client Tier, 즉 VS.NET 2005가 설치된 컴퓨터도 ActiveDirectory에 참여를 해야 합니다.
81정성태12/31/200420346    답변글 VS.NET IDE: 12.2. Tier 컴퓨터를 모두 영문으로 재구성
109정성태3/4/200515559    답변글 VS.NET IDE: 12.3. [보완] MS 공식 아티클 - Installing the December CTP Release of Visual Studio Team System
73정성태11/14/200517385.NET Framework: 19. VS.NET 2005 Team Foundation Server 설치오류 - 26204 예외
72정성태12/26/200418862.NET Framework: 18. .NET Framework 2.0 Beta 설치 후에 Windows SharePoint Service 오류 [1]
136정성태3/31/200518721    답변글 .NET Framework: 18.1. Windows Sharepoint Services 를 설치한 이후 ASP.NET 오류 문제
71정성태12/26/200417079VS.NET IDE: 11. SQL Server 2005 Beta 2 를 네트워크 드라이브로부터 설치시 오류
70정성태12/26/200419896VS.NET IDE: 10. WSS 설치 후 localhost 접근 보안 오류
69정성태12/5/200416985VS.NET IDE: 9. 다른 컴퓨터(방화벽 설치)에 설치된 SQL Server에 통합 인증을 할 때 필요한 포트
68정성태10/31/200421967.NET Framework: 17. Win32_NTLogEvent를 c#에서 wmi 쿼리할 때..에러..
... 181  182  183  184  185  186  187  188  189  190  191  192  193  [194]  195  ...