Microsoft MVP성태의 닷넷 이야기
닷넷: 2340. C# - Win32 Multimedia Timer 주기 [링크 복사], [링크+제목 복사],
조회: 537
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

C# - Win32 Multimedia Timer 주기

아래와 같은 질문이 있군요. ^^

멀티미디어 타이머의 일정하지 않은 호출 주기에 관한 질문
; https://www.sysnet.pe.kr/3/0/5974

질문을 위한 테스트 코드의 구성을 보면,

using System.Diagnostics;
using System.Runtime.InteropServices;

class MultimediaTimerTest
{
    const int TIME_PERIODIC = 0x0001;
    const int TIME_KILL_SYNCHRONOUS = 0x0100;

    private delegate void TimerCallback(uint uTimerID, uint uMsg, IntPtr dwUser, IntPtr dw1, IntPtr dw2);

    [DllImport("winmm.dll", SetLastError = true)]
    static extern uint timeSetEvent(uint msDelay, uint msResolution, TimerCallback callback, IntPtr userCtx, uint eventType);

    [DllImport("winmm.dll", SetLastError = true)]
    static extern uint timeKillEvent(uint timerId);

    [DllImport("winmm.dll", SetLastError = true)]
    static extern uint timeBeginPeriod(uint uMilliseconds);

    [DllImport("winmm.dll", SetLastError = true)]
    static extern uint timeEndPeriod(uint uMilliseconds);

    static TimerCallback? _callback;
    static uint _timerId;

    static Stopwatch _stopwatch = Stopwatch.StartNew();
    static long _lastTicks;
    static double _tickFrequencyMs = 1000.0 / Stopwatch.Frequency;

    static double _minInterval = double.MaxValue;
    static double _maxInterval = double.MinValue;

    static Stopwatch _statReset = Stopwatch.StartNew();

    static void Main()
    {
        uint msDelay = 1; // 1ms 주기

        Console.WriteLine($"▶ Multimedia Timer Test 시작 ({msDelay}ms 주기)");
        timeBeginPeriod(1);

        _callback = TimerTick;
        _timerId = timeSetEvent(msDelay, 0, _callback, IntPtr.Zero, TIME_PERIODIC | TIME_KILL_SYNCHRONOUS);

        Console.WriteLine("Press CTRL+C key to exit...");

        Console.CancelKeyPress += (s, e) =>
        {
            Console.WriteLine("\n종료 중...");
            timeKillEvent(_timerId);
            timeEndPeriod(1);
            Thread.Sleep(100);
        };

        Thread.Sleep(Timeout.Infinite);
    }

    static void TimerTick(uint id, uint msg, IntPtr user, IntPtr param1, IntPtr param2)
    {
        long now = _stopwatch.ElapsedTicks;
        double interval = (now - _lastTicks) * _tickFrequencyMs;
        _lastTicks = now;

        if (interval > 0.01)
        {
            if (interval < _minInterval) _minInterval = interval;
            if (interval > _maxInterval) _maxInterval = interval;
        }

        if (_statReset.ElapsedMilliseconds >= 3000)
        {
            Console.WriteLine($"[3초 통계] 최소: {_minInterval:F4} ms / 최대: {_maxInterval:F4} ms");
            _minInterval = double.MaxValue;
            _maxInterval = double.MinValue;
            _statReset.Restart();
        }
    }
}

3초마다 지난 TimerTick 시간부터 얼마나 빨리/느리게 호출되었는지를 재고 있는데요, 제 컴퓨터에서 실행해 보면 대충 이런 결과가 나옵니다.

▶ Multimedia Timer Test 시작 (1ms 주기)
Press any key to exit...
[3초 통계] 최소: 0.0116 ms / 최대: 2.9326 ms
[3초 통계] 최소: 0.2317 ms / 최대: 11.6624 ms
[3초 통계] 최소: 0.4911 ms / 최대: 2.0578 ms
[3초 통계] 최소: 0.4918 ms / 최대: 2.0510 ms
[3초 통계] 최소: 0.4048 ms / 최대: 2.1614 ms
[3초 통계] 최소: 0.3551 ms / 최대: 2.2452 ms
[3초 통계] 최소: 0.4750 ms / 최대: 2.6287 ms
[3초 통계] 최소: 0.5099 ms / 최대: 2.0470 ms
[3초 통계] 최소: 0.4998 ms / 최대: 2.1595 ms
[3초 통계] 최소: 0.4815 ms / 최대: 2.0719 ms
[3초 통계] 최소: 0.4523 ms / 최대: 2.0096 ms
[3초 통계] 최소: 0.4480 ms / 최대: 2.0150 ms
...[생략]...

대체로 0.4ms ~ 2.0ms 사이에서 호출이 되는데요, 다시 말해 Timer를 1ms 주기로 설정했어도 실제 호출 주기는 경우에 따라 0.4ms 후에 빠르게 호출될 수도, 혹은 반대로 2.0ms 후에 느리게 호출될 수도 있다는 것입니다.

사실 이건 Windows의 Multimedia Timer가 결국에는 TimerTick이라는 콜백 함수를 운영체제의 스케줄링에 영향을 받아 호출하게 되므로 어쩔 수 없는 현상입니다. (만약 저걸 정확하게 지켜야 한다면 Windows가 아닌 다른 RTOS를 선택해야 합니다.)

게다가 timeBeginPeriod도 Timer Interrupt의 주기를 조정하는 함수이기 때문에,

윈도우 운영체제의 시간 함수 (1) - GetTickCount와 timeGetTime의 차이점
; https://www.sysnet.pe.kr/2/0/11063

전에 설명했던 현상이 그대로 재현되는 것이나 마찬가지입니다.




그렇긴 한데, RTOS도 아닌 Windows가 제공하는 멀티미디어 타이머의 의미는 시간 주기를 보정해 준다는 점입니다. 이에 대해서는 전에 아래의 Q&A로 의견이 오간 적이 있었습니다. ^^

WaitHandle.WaitOne 과 Stopwatch에 관한 질문
; https://www.sysnet.pe.kr/3/0/956

예를 들어, 1초 단위로 주기를 설정했어도 어느 순간 1.001초 후에 호출되었다면, 그다음의 호출은 0.999초 후에 호출되도록 보정된다는 것이 Multimedia Timer의 특징입니다. 단지, 질문자의 예제에서는 스케줄러에게는 너무 벅찬 1ms 주기로 timer를 설정했기 때문에 오차가 너무 발생한 것입니다.

실제로 주기 자체를 1000으로 바꿔서 테스트하면,

uint msDelay = 1000; 

실행 시 이런 결과가 나오는데,

[3초 통계] 최소: 999.5577 ms / 최대: 1000.3119 ms
[3초 통계] 최소: 999.4849 ms / 최대: 1000.2293 ms
[3초 통계] 최소: 999.8198 ms / 최대: 1000.2886 ms
[3초 통계] 최소: 999.1548 ms / 최대: 1000.4207 ms
[3초 통계] 최소: 999.5312 ms / 최대: 1000.7725 ms
[3초 통계] 최소: 999.8065 ms / 최대: 1000.0304 ms
[3초 통계] 최소: 999.5189 ms / 최대: 1000.7229 ms
[3초 통계] 최소: 999.2757 ms / 최대: 1000.8761 ms
[3초 통계] 최소: 999.0815 ms / 최대: 1000.8589 ms
...[생략]...

보는 바와 같이 이번엔 주기에 비해 발생하는 오차가 상대적으로 크지 않은 수준으로 내려왔습니다.




테스트하는 김에 좀 더 알아볼까요? 위의 출력 결과에 Thread.CurrentThread.ManagedThreadId를 함께 나오도록 하면,

Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] [3초 통계] 최소: {_minInterval:F4} ms / 최대: {_maxInterval:F4} ms");

스레드 ID가 동일하게 출력이 되는데요, 달리 말하면 TimerTick 콜백을 호출하는 스레드가 항상 정해져 있다는 것을 의미합니다. 이 상태에서 1ms 주기로 설정해 1초마다 몇 번 실행되는지 테스트를 다음과 같이 할 수 있습니다.

static void Main()
{
    timeBeginPeriod(1);

    Thread t = new Thread(threadPrint);
    t.IsBackground = true;
    t.Start(); // 별도 스레드에서 수행 횟수 출력

    // ...[생략]...

    Thread.Sleep(Timeout.Infinite);
}

static long _count = 0;

private static void threadPrint(object? obj)
{
    Stopwatch st = new Stopwatch();
    st.Start();
    long oldTime = st.ElapsedTicks;

    while (true)
    {
        long now = st.ElapsedTicks;
        long timeDiff = now - oldTime;

        if (timeDiff >= 10_000_000)
        {
            long count = Interlocked.Exchange(ref _count, 0);
            oldTime = now;

            // 1초마다 몇 번 호출되었는지 출력
            Console.WriteLine($"1000 ms ==> {count}");
        }
    }
}

static void TimerTick(uint id, uint msg, IntPtr user, IntPtr param1, IntPtr param2)
{
    Interlocked.Increment(ref _count); // 실행 횟수 누적
}

실행해 보면,

1000 ms ==> 999
1000 ms ==> 1001
1000 ms ==> 999
1000 ms ==> 1001
1000 ms ==> 1000
1000 ms ==> 999
1000 ms ==> 1001
1000 ms ==> 1000
1000 ms ==> 1000
1000 ms ==> 1000
1000 ms ==> 999
1000 ms ==> 1001
1000 ms ==> 1000
1000 ms ==> 1000
1000 ms ==> 1000
1000 ms ==> 1000
1000 ms ==> 1000
1000 ms ==> 999
1000 ms ==> 1000
1000 ms ==> 1001
1000 ms ==> 1000
1000 ms ==> 1000
...[생략]...

보는 바와 같이, 1초에 (간혹 밀려서) 999번이 호출되었다고 해도 이후에 (밀린 시간 대비 빠르게 호출한 경우로 인해) 1001번이 호출되고 있습니다. 하지만 그렇다고 이것이 반드시 (평균적으로) 1,000번 호출된다는 보장은 없습니다. 왜냐하면, TimerTick의 내부 수행 시간이 1ms를 넘게 되면, 그다음 호출이 밀리게 되기 때문입니다. (위의 예제 코드는 _count를 증가하는 간단한 코드만 있기 때문에 그런 현상이 발생하지 않는 것입니다.)

이것을 CPU 부하를 이용한 uSleep을 호출하도록 수정해 확인할 수 있습니다.

// 이 핸들러의 주기를 1ms로 설정했지만,
static void TimerTick(uint id, uint msg, IntPtr user, IntPtr param1, IntPtr param2)
{
    Interlocked.Increment(ref _count);

    NativeMethods.uSleep(5000); // 일부러 5000us, 즉 5ms 동안 소비
                                // Thread.Sleep(5)로 사용하지 않은 이유가 뭘까요? ^^
}

실행하면, 거의 정확히 저 비율대로 호출 횟수가 줄어드는데,

1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 199
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
1000 ms ==> 200
...[생략]...

그래도 저렇게 199번이 한 번씩 나오면서 이후 201번으로 상쇄되는 현상은 없습니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




정리해 보면, 멀티미디어 타이머는 1) 주어진 시간 주기로 반드시 호출하는 것은 아니고, 주어진 시간 주기로 호출된 것처럼 보정하는 효과가 있으며, 2) 만약 콜백 메서드의 수행 시간이 주기보다 길어지면 그다음 호출이 밀리게 돼 주기에 비해 호출 횟수가 줄어들 수 있습니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/11/2025]

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

비밀번호

댓글 작성자
 




... 106  107  108  109  110  111  112  113  114  115  116  117  [118]  119  120  ...
NoWriterDateCnt.TitleFile(s)
11017정성태8/9/201627854웹: 33. HTTP 쿠키에 한글 값을 설정하는 방법
11016정성태8/7/201626057개발 환경 구성: 291. Windows Server Containers 소개
11015정성태8/7/201624418오류 유형: 348. Windows Server 2016 TP5에서 Windows Containers의 docker run 실행 시 encountered an error during Start failed in Win32
11014정성태8/6/201625074오류 유형: 347. Hyper-V Virtual Machine Management service Account does not have permission to open attachment
11013정성태8/6/201635940개발 환경 구성: 290. Windows 10에서 경험해 보는 Windows Containers와 docker [4]
11012정성태8/6/201625957오류 유형: 346. Windows 10에서 Windows Containers의 docker run 실행 시 encountered an error during CreateContainer failed in Win32 발생
11011정성태8/6/201627382기타: 59. outlook.live.com 메일 서비스의 아웃룩 POP3 설정하는 방법
11010정성태8/6/201624307기타: 58. Outlook에 설정한 SMTP/POP3(예:천리안 메일) 계정 암호를 잊어버린 경우
11009정성태8/3/201629513개발 환경 구성: 289. 2016-08-02부터 시작된 윈도우 10 1주년 업데이트에서 Bash Shell 사용 [8]
11008정성태8/1/201623903오류 유형: 345. 2의 30승 이상의 원소를 갖는 경우 버그가 발생하는 이진 검색(Binary Search) 코드
11007정성태8/1/201625564오류 유형: 344. RDP ActiveX 컨트롤로 특정 PC에 연결할 수 없을 때, 오류 상황을 해결하기 위한 팁파일 다운로드1
11006정성태7/22/201628860개발 환경 구성: 288. SSL 인증서를 Azure Cloud Service에 적용하는 방법
11005정성태7/22/201627437개발 환경 구성: 287. Let's Encrypt 인증서 업데이트 주기: 90일
11004정성태7/22/201622055오류 유형: 343. Invalid service definition or service configuration. Please see the Error List for more details.
11003정성태7/20/201629580VS.NET IDE: 110. Visual Studio 2015에서 .NET Core 응용 프로그램 개발 [1]
11002정성태7/20/201622921개발 환경 구성: 286. Microsoft Azure 서비스의 구독은 반드시 IE로!
11001정성태7/19/201634022.NET Framework: 599. .NET Core/SDK 설치 및 기본 사용법 [6]
11000정성태7/16/201622704오류 유형: 342. Microsoft Visual Studio 2010 Tools for Office Runtime (x86 and x64) 설치 시 오류
10999정성태7/16/201623384오류 유형: 341. .NET Framework 4.5.2가 설치 안 되는 경우
10998정성태7/16/201623009.NET Framework: 598. C# - Excel 시트에 윈도우 폼 기능을 추가하는 방법 [1]파일 다운로드1
10997정성태7/16/201623455오류 유형: 340. HTTP Error 500.23 - Internal Server Error파일 다운로드1
10996정성태7/14/201628603Windows: 118. 유선 접속 상태에서 재부팅하면 무선 연결이 자동 연결 안되는 문제 [4]파일 다운로드1
10995정성태6/27/201622013VS.NET IDE: 109. Visual Studio 유료 버전 사용자의 주기적인 온라인 인증을 없애는 방법
10994정성태6/23/201621331개발 환경 구성: 285. 알고스팟(https://algospot.com)을 위한 Visual C++ 답안 작성 요령파일 다운로드1
10993정성태6/23/201622528.NET Framework: 597. 닷넷 메타데이터에 struct/class(값/참조 형식)의 구분이 있을까요?
10992정성태6/13/201619053오류 유형: 339. vbs 스크립트 실행 시 항상 실행 여부를 묻는 질문 창이 뜬다면?
... 106  107  108  109  110  111  112  113  114  115  116  117  [118]  119  120  ...