Microsoft MVP성태의 닷넷 이야기
닷넷: 2340. C# - Win32 Multimedia Timer 주기 [링크 복사], [링크+제목 복사],
조회: 436
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 181  182  183  184  [185]  186  187  188  189  190  191  192  193  194  195  ...
NoWriterDateCnt.TitleFile(s)
397정성태11/11/200626837    답변글 .NET Framework: 75.6. CardWriter.csproj와 함께 알아보는 인증서 식별 방법(이 글의 내용은 재작성되어질 예정입니다.)
398정성태11/12/200625305    답변글 .NET Framework: 75.7. 카드에 암호 거는 방법(이 글의 내용은 재작성되어질 예정입니다.)
399정성태11/12/200627457    답변글 .NET Framework: 75.8. 인증서/스마트 카드에 기반한 Managed Card - STS 구현(이 글의 내용은 재작성되어질 예정입니다.) [5]
369정성태10/22/200622925오류 유형: 15. 자동 업데이트 실패
367정성태10/22/200638870Windows: 3. IIS 7.0 다중 바인딩 설정하는 방법 [1]
365정성태10/21/200622209Windows: 2. 서버(build 5600)에 IIS 7.0 서비스와 .NET 3.0 설치 방법
359정성태10/17/200618363오류 유형: 14. VS.NET 빌드 오류 - FxCopCmd.exe returned error code 65.
358정성태10/17/200623802오류 유형: 13. WSE 3.0 서비스 관련 WSE101 오류 / Destination Unreachable
357정성태12/1/200626064.NET Framework: 74. WCF 이야기 [4]
378정성태10/28/200630947    답변글 .NET Framework: 74.1. WCF와 WSE 3.0의 활용 [4]파일 다운로드1
379정성태11/3/200629803    답변글 .NET Framework: 74.2. WCF로 구현하는 .NET Remoting [4]파일 다운로드1
380정성태10/28/200628679    답변글 .NET Framework: 74.3. 웹 서비스와 닷넷 리모팅으로써의 WCF 구현파일 다운로드1
381정성태10/28/200631088    답변글 .NET Framework: 74.4. WCF 서비스 참조 추가 메뉴 [2]
382정성태10/28/200637068    답변글 .NET Framework: 74.5. WCF 서비스를 IIS에서 호스팅하는 방법파일 다운로드1
383정성태10/28/200632016    답변글 .NET Framework: 74.6. IIS 6.0: 다중 Endpoint 제공파일 다운로드1
384정성태10/28/200628980    답변글 .NET Framework: 74.7. IIS 7.0: 다중 Endpoint 제공
389정성태11/11/200631790    답변글 .NET Framework: 74.8. WCF에 SSL 적용 (1) - Httpcfg.exe 도구를 이용한 SSL 설정
390정성태11/6/200628887    답변글 .NET Framework: 74.9. WCF에 SSL 적용 (2) - 서비스 제작파일 다운로드1
356정성태10/7/200624533COM 개체 관련: 19. COM의 Apartment를 이해해 보자. [8]
386light10/30/200619484    답변글 COM 개체 관련: 19.1. [답변]: COM 객체를 글로벌마샬으로 만든후, 사용한다.
355정성태10/9/200627118개발 환경 구성: 19. Internet_Zone 하위에 새로운 코드 그룹을 추가하는 예제 [4]파일 다운로드2
353정성태12/31/200635409개발 환경 구성: 18. 윈도우즈 인증서 서비스 이야기 [3]
354정성태10/23/200637780    답변글 개발 환경 구성: 18.1. 윈도우즈 인증서 서비스 설치
372정성태12/31/200639944    답변글 개발 환경 구성: 18.2. 웹 사이트에 SSL을 적용 [3]
373정성태10/24/200631446    답변글 개발 환경 구성: 18.3. 사용자 입장에서의 HTTPS 접근 (1)
374정성태10/25/200628772    답변글 개발 환경 구성: 18.4. 사용자 입장에서의 HTTPS 접근 (2)
... 181  182  183  184  [185]  186  187  188  189  190  191  192  193  194  195  ...