timeBeginPeriod 不适用于 Intel Comet Lake CPU (i5 10400H)

Posted

技术标签:

【中文标题】timeBeginPeriod 不适用于 Intel Comet Lake CPU (i5 10400H)【英文标题】:timeBeginPeriod not working on Intel Comet Lake CPU (i5 10400H) 【发布时间】:2020-11-02 12:43:02 【问题描述】:

我的应用程序中有一些依赖于短计时器的操作。使用下面的示例代码,我可以根据需要每隔约 5 毫秒触发一次计时器。

在 Intel i5 10400H CPU 上,观察到计时关闭,并且回调发生在约 15 毫秒(或 15 的倍数)之后。使用 ClockRes sysinternals 工具显示,即使在以下代码中调用 timeBeginPeriod(1) 之后运行,机器的系统计时器分辨率也为 15 毫秒。

使用https://cms.lucashale.com/timer-resolution/ 将分辨率设置为支持的最大值(0.5ms)不会改变示例代码的行为。

据我所见,机器正在使用 Invariant TSC acpi 计时器,并且强制它使用 HPET(使用 bcdedit /set useplatformclock true 并重新启动)并没有改变行为。

我在 CPU 文档或勘误表中看不到任何可以解释这一点的内容。

我不知道问题出在哪里,如果它是我可以解决的问题,有什么想法吗?

编辑:打开此程序 (DPC Latency Checker) 会导致定时器队列按预期触发,因此可以解决。

示例代码:

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

namespace ConsoleApp1

    class Program
    
        static void Main(string[] args)
        
            using (new TimePeriod(1))
                RunTimer();
        

        public static void RunTimer()
        
            var completionEvent = new ManualResetEvent(false);
            var stopwatch = Stopwatch.StartNew();
            var i = 0;
            var previous = 0L;
            using var x = TimerQueue.Default.CreateTimer((s) =>
            
                if (i > 100)
                    completionEvent.Set();
                i++;
                var now = stopwatch.ElapsedMilliseconds;
                var gap = now - previous;
                previous = now;
                Console.WriteLine($"Gap: gapms");
            , "", 10, 5);
            completionEvent.WaitOne();
        
    

    public class TimerQueueTimer : IDisposable
    
        private TimerQueue MyQueue;
        private TimerCallback Callback;
        private object UserState;
        private IntPtr Handle;

        internal TimerQueueTimer(
            TimerQueue queue,
            TimerCallback cb,
            object state,
            uint dueTime,
            uint period,
            TimerQueueTimerFlags flags)
        
            MyQueue = queue;
            Callback = cb;
            UserState = state;
            bool rslt = TQTimerWin32.CreateTimerQueueTimer(
                out Handle,
                MyQueue.Handle,
                TimerCallback,
                IntPtr.Zero,
                dueTime,
                period,
                flags);
            if (!rslt)
            
                throw new Win32Exception(Marshal.GetLastWin32Error(), "Error creating timer.");
            
        
        ~TimerQueueTimer()
        
            Dispose(false);
        
        public void Change(uint dueTime, uint period)
        
            bool rslt = TQTimerWin32.ChangeTimerQueueTimer(MyQueue.Handle, ref Handle, dueTime, period);
            if (!rslt)
            
                throw new Win32Exception(Marshal.GetLastWin32Error(), "Error changing timer.");
            
        
        private void TimerCallback(IntPtr state, bool bExpired)
        
            Callback.Invoke(UserState);
        
        public void Dispose()
        
            Dispose(true);
            GC.SuppressFinalize(this);
        
        private IntPtr completionEventHandle = new IntPtr(-1);
        public void Dispose(WaitHandle completionEvent)
        
            completionEventHandle = completionEvent.SafeWaitHandle.DangerousGetHandle();
            this.Dispose();
        
        private bool disposed = false;
        protected virtual void Dispose(bool disposing)
        
            if (!disposed)
            
                bool rslt = TQTimerWin32.DeleteTimerQueueTimer(MyQueue.Handle,
                    Handle, completionEventHandle);
                if (!rslt)
                
                    throw new Win32Exception(Marshal.GetLastWin32Error(), "Error deleting timer.");
                
                disposed = true;
            
        
    

    public class TimerQueue : IDisposable
    
        public IntPtr Handle  get; private set; 
        public static TimerQueue Default  get; private set; 
        static TimerQueue()
        
            Default = new TimerQueue(IntPtr.Zero);
        
        private TimerQueue(IntPtr handle)
        
            Handle = handle;
        
        public TimerQueue()
        
            Handle = TQTimerWin32.CreateTimerQueue();
            if (Handle == IntPtr.Zero)
            
                throw new Win32Exception(Marshal.GetLastWin32Error(), "Error creating timer queue.");
            
        
        ~TimerQueue()
        
            Dispose(false);
        
        public TimerQueueTimer CreateTimer(
            TimerCallback callback,
            object state,
            uint dueTime,
            uint period)
        
            return CreateTimer(callback, state, dueTime, period, TimerQueueTimerFlags.ExecuteInPersistentThread);
        

        public TimerQueueTimer CreateTimer(
            TimerCallback callback,
            object state,
            uint dueTime,
            uint period,
            TimerQueueTimerFlags flags)
        
            return new TimerQueueTimer(this, callback, state, dueTime, period, flags);
        

        private IntPtr CompletionEventHandle = new IntPtr(-1);

        public void Dispose()
        
            Dispose(true);
            GC.SuppressFinalize(this);
        

        public void Dispose(WaitHandle completionEvent)
        
            CompletionEventHandle = completionEvent.SafeWaitHandle.DangerousGetHandle();
            Dispose();
        

        private bool Disposed = false;

        protected virtual void Dispose(bool disposing)
        
            if (!Disposed)
            
                if (Handle != IntPtr.Zero)
                
                    bool rslt = TQTimerWin32.DeleteTimerQueueEx(Handle, CompletionEventHandle);
                    if (!rslt)
                    
                        int err = Marshal.GetLastWin32Error();
                        throw new Win32Exception(err, "Error disposing timer queue");
                    
                
                Disposed = true;
            
        
    

    public enum TimerQueueTimerFlags : uint
    
        ExecuteDefault = 0x0000,
        ExecuteInTimerThread = 0x0020,
        ExecuteInIoThread = 0x0001,
        ExecuteInPersistentThread = 0x0080,
        ExecuteLongFunction = 0x0010,
        ExecuteOnlyOnce = 0x0008,
        TransferImpersonation = 0x0100,
    

    public delegate void Win32WaitOrTimerCallback(
        IntPtr lpParam,
        [MarshalAs(UnmanagedType.U1)] bool bTimedOut);

    static public class TQTimerWin32
    
        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static IntPtr CreateTimerQueue();

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool DeleteTimerQueue(IntPtr timerQueue);

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool DeleteTimerQueueEx(IntPtr timerQueue, IntPtr completionEvent);

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool CreateTimerQueueTimer(
            out IntPtr newTimer,
            IntPtr timerQueue,
            Win32WaitOrTimerCallback callback,
            IntPtr userState,
            uint dueTime,
            uint period,
            TimerQueueTimerFlags flags);

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool ChangeTimerQueueTimer(
            IntPtr timerQueue,
            ref IntPtr timer,
            uint dueTime,
            uint period);

        [DllImport("kernel32.dll", SetLastError = true)]
        public extern static bool DeleteTimerQueueTimer(
            IntPtr timerQueue,
            IntPtr timer,
            IntPtr completionEvent);
    

    public sealed class TimePeriod : IDisposable
    
        private const string WINMM = "winmm.dll";

        private static TIMECAPS timeCapabilities;

        private static int inTimePeriod;

        private readonly int period;

        private int disposed;

        [DllImport(WINMM, ExactSpelling = true)]
        private static extern int timeGetDevCaps(ref TIMECAPS ptc, int cbtc);

        [DllImport(WINMM, ExactSpelling = true)]
        private static extern int timeBeginPeriod(int uPeriod);

        [DllImport(WINMM, ExactSpelling = true)]
        private static extern int timeEndPeriod(int uPeriod);

        static TimePeriod()
        
            int result = timeGetDevCaps(ref timeCapabilities, Marshal.SizeOf(typeof(TIMECAPS)));
            if (result != 0)
            
                throw new InvalidOperationException("The request to get time capabilities was not completed because an unexpected error with code " + result + " occured.");
            
        

        internal TimePeriod(int period)
        
            if (Interlocked.Increment(ref inTimePeriod) != 1)
            
                Interlocked.Decrement(ref inTimePeriod);
                throw new NotSupportedException("The process is already within a time period. Nested time periods are not supported.");
            

            if (period < timeCapabilities.wPeriodMin || period > timeCapabilities.wPeriodMax)
            
                throw new ArgumentOutOfRangeException("period", "The request to begin a time period was not completed because the resolution specified is out of range.");
            

            int result = timeBeginPeriod(period);
            if (result != 0)
            
                throw new InvalidOperationException("The request to begin a time period was not completed because an unexpected error with code " + result + " occured.");
            

            this.period = period;
        

        internal static int MinimumPeriod
        
            get
            
                return timeCapabilities.wPeriodMin;
            
        

        internal static int MaximumPeriod
        
            get
            
                return timeCapabilities.wPeriodMax;
            
        

        internal int Period
        
            get
            
                if (this.disposed > 0)
                
                    throw new ObjectDisposedException("The time period instance has been disposed.");
                

                return this.period;
            
        

        public void Dispose()
        
            if (Interlocked.Increment(ref this.disposed) == 1)
            
                timeEndPeriod(this.period);
                Interlocked.Decrement(ref inTimePeriod);
            
            else
            
                Interlocked.Decrement(ref this.disposed);
            
        

        [StructLayout(LayoutKind.Sequential)]
        private struct TIMECAPS
        
            internal int wPeriodMin;

            internal int wPeriodMax;
        
    


【问题讨论】:

可能是主板驱动问题。从你的供应商那里检查有最新的。请参阅以下内容:forums.tomshardware.com/threads/… 5ms 对于多进程操作系统来说是相当紧张的。在我之前从事过的具有类似要求的项目(工业控制等)中,我们必须指定要使用的确切 MB/Proc,并测试了许多不得不拒绝的组合。 出现问题的机器是戴尔 Latitude 5511 笔记本电脑,戴尔更新软件声称所有驱动程序都是最新的。 Windows 有所有更新 目前使用的机器有很多不同型号,并且没有其他机器在不用于其他任务(即 SOP)时存在时间问题。 【参考方案1】:

这似乎是issue with windows 10 2004。我猜它与处理器/主板无关。

可能的解决方法是使用stopwatch and spinwait on a thread。这对于常规的消费者应用程序来说是不可取的,因为它会消耗一个完整的线程,但如果您可以完全控制系统,这可能是可行的。

【讨论】:

【参考方案2】:

我在 Windows 10 2004 下遇到了完全相同的问题。以前的版本似乎没有表现出相同的行为。 CreateTimerQueueTimer 似乎不再遵守 timeBeginPeriod 了,它的最短周期似乎是 15 毫秒(好旧的 15 毫秒...)。

有一些人抱怨这个问题,但不是很多。 (例如,参见this forum entry。 我不知道这是 v2004 中引入的错误,还是偷偷溜过去的省电“功能”。 话虽如此,官方文档从未将 TimerQueueTimers 和 timeBeginPeriod 联系起来,所以如果一开始可能是一个错误,他们尊重 timeBeginPeriod 设置。

无论如何,我最终在 timeBeginPeriod/timeSetEvent 之上重新实现了一个 TimerQueue 以实现所需的计时器频率。

【讨论】:

timeSetEvent 不是也有同样的问题吗? 不,不是。这就是为什么我认为这是一个“更正”。如果您查看 TimerQueues 的文档,它并没有指出与 time* API 的任何链接,尤其是 timeBeginPeriod/timeEndPeriod。受影响的 TimerQueues 可能是实施的副产品,并且已在 2004 年出于功耗目的“更正”。 谢谢,知道这非常有用。所以他们仍在唤醒内核以尊重timeSetEvent,但他们现在避免重新安排许多以前的进程。希望他们在某处记录更改,即使是非正式的。 再一次,这只是一种直觉。这可能只是一个普通的错误,上帝知道我们已经在 Windows 10 中分享了这些问题......但我没有看到任何承认问题的方式......【参考方案3】:

遇到同样的问题,我正在使用CreateTimerQueueTimer。仍然有效的是timeSetEvent。你会失去一些精确度,因为它在整毫秒内,但总比没有好。

【讨论】:

以上是关于timeBeginPeriod 不适用于 Intel Comet Lake CPU (i5 10400H)的主要内容,如果未能解决你的问题,请参考以下文章

我调用了 timeBeginPeriod(1),但它没有用

代号一。 IOS 拦截 URL 问题。

Tensorflow - 多 GPU 不适用于模型(输入),也不适用于计算梯度

为啥 UITableViewAutomaticDimension 不适用于 sectionFooterHeight?

为啥排序不适用于矢量?

验证不适用于 saveMany