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

비밀번호

댓글 작성자
 




... 151  152  153  154  155  [156]  157  158  159  160  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1195정성태12/8/201149876오류 유형: 143. DXSDK_Jun10.exe 설치 시 "Error Code: S1023" 오류 해결하는 방법 [4]
1194정성태12/8/201137671개발 환경 구성: 137. Visual C++ 런타임 구성요소에 대한 디버그 버전 설치하는 방법
1193정성태12/8/201124787오류 유형: 142. Windows Phone SDK 7.1 설치 시 Expression Blend 제거를 요구하는 경우
1192정성태12/8/201127759개발 환경 구성: 136. Windows 7 SP1의 IIS에서 사용자 프로파일을 로드하는 방법
1191정성태12/6/201128828.NET Framework: 280. MVC3에서 JavaScriptSerializer 재정의하는 방법파일 다운로드1
1190정성태12/6/201131915오류 유형: 141. Visual C++ 컴파일 오류 - error C2275: 'xxxxx' : illegal use of this type as an expression [1]
1189정성태12/6/201139316VS.NET IDE: 70. Visual Studio에서 프로젝트 로드가 안된다면?
1188정성태12/3/201128159개발 환경 구성: 135. 마이크로소프트 TFS 호스팅 서비스 - Preview [3]
1187정성태12/2/201133034개발 환경 구성: 134. Robocopy 오류 및 종료 코드
1186정성태12/1/201135086.NET Framework: 279. WPF - 그리기 성능 및 Blurring 문제파일 다운로드1
1185정성태11/29/201124901.NET Framework: 278. WPF - Content의 Changed 이벤트에 해당하는게 뭔가요?파일 다운로드1
1184정성태11/29/201128238.NET Framework: 277. F#과 WPF가 어울리지 못하는 근본적인 이유 [2]
1183정성태11/26/201123304오류 유형: 140. Visual Studio 2010 - Floating된 에디트 윈도우가 사라지지 않는 경우 [2]
1182정성태11/25/201159541.NET Framework: 276. 중복 없는 숫자를 랜덤으로 배열하는 방법 [5]파일 다운로드1
1181정성태11/24/201129755디버깅 기술: 44. windbg의 mscordacwks DLL 로드 문제
1180정성태11/23/201139557.NET Framework: 275. 레지스트리 등록 및 Interop DLL 없이 COM 개체 사용하는 방법 [2]파일 다운로드1
1179정성태11/22/201130103.NET Framework: 274. ReaderWriterLockSlim은 언제 쓰는 걸까요? [4]파일 다운로드1
1178정성태11/19/201126705.NET Framework: 273. 설치된 .NET 버전에 민감한 코드를 포함하는 경우, 다중으로 어셈블리를 만들어야 할까요?파일 다운로드1
1177정성태11/18/201131986.NET Framework: 272. 소켓 연결 시간 제한 - 두 번째 이야기 [1]파일 다운로드1
1176정성태11/17/201131417.NET Framework: 271. C#에서 확인해 보는 관리 힙의 인스턴스 구조 [3]파일 다운로드1
1175정성태11/16/201129366.NET Framework: 270. .NET 참조 개체 인스턴스의 Object Header를 확인하는 방법 [1]파일 다운로드1
1174정성태11/15/201128693.NET Framework: 269. 일반 참조형의 기본 메모리 소비는 얼마나 될까요? [4]
1173정성태11/14/201124708.NET Framework: 268. .NET Array는 왜 12bytes의 기본 메모리를 점유할까? [1]
1172정성태11/13/201121706.NET Framework: 267. windbg - GC Heap에서 .NET 타입에 대한 배열을 찾는 방법
1171정성태11/12/201138514.NET Framework: 266. StringBuilder에서의 OutOfMemoryException 오류 원인 분석 [4]파일 다운로드1
1170정성태11/10/201127915.NET Framework: 265. Named 동기화 개체 생성 시 System.UnauthorizedAccessException 예외 발생하는 경우
... 151  152  153  154  155  [156]  157  158  159  160  161  162  163  164  165  ...