timeBeginPeriod 不适用于 Intel Comet Lake CPU (i5 10400H)
timeBeginPeriod not working on Intel Comet Lake CPU (i5 10400H)
我的应用程序中有一些操作依赖于较短的计时器。使用下面的示例代码,我根据需要每 ~5 毫秒触发一次计时器。
在 Intel i5 10400H CPU 上观察到时序关闭,回调发生在 ~15 毫秒(或 15 的倍数)之后。使用 ClockRes sysinternals 工具表明,即使在下面的代码中调用 timeBeginPeriod(1)
之后 运行 时,机器的系统计时器分辨率仍为 15ms。
使用 https://cms.lucashale.com/timer-resolution/ 将分辨率设置为最大支持值 (0.5ms) 不会更改示例代码的行为。
据我所知,机器正在使用不变的 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: {gap}ms");
}, "", 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;
}
}
}
这似乎是 issue with windows 10 2004。我猜它与 processor/motherboard.
无关
可能的解决方法是使用 stopwatch and spinwait on a thread。这对于常规消费者应用程序是不可取的,因为它会消耗一个完整的线程,但如果您对系统有完全控制权,这可能是可行的。
运行同样的问题,我用的是CreateTimerQueueTimer
。仍然有效的是 timeSetEvent
。你会失去一些精度,因为它是整毫秒,但总比没有好。
我在 Windows 10 2004 下遇到了完全相同的问题。以前的版本似乎没有表现出相同的行为。 CreateTimerQueueTimer 似乎不再支持 timeBeginPeriod,它的最小周期似乎是 15 毫秒(好旧的 15 毫秒...)。
周围有一些人抱怨这个问题,但不是很多。 (例如,参见 this forum entry。
我不知道这是 v2004 中引入的错误,还是我们偷偷摸摸的 power-saving“功能”。
话虽这么说,官方文档从未将 TimerQueueTimers 和 timeBeginPeriod 联系起来,所以如果一开始可能是一个错误,他们就会尊重 timeBeginPeriod 设置。
无论如何,我最终 re-implementing 在 timeBeginPeriod/timeSetEvent 之上添加了一个 TimerQueue 以达到所需的计时器频率。
我的应用程序中有一些操作依赖于较短的计时器。使用下面的示例代码,我根据需要每 ~5 毫秒触发一次计时器。
在 Intel i5 10400H CPU 上观察到时序关闭,回调发生在 ~15 毫秒(或 15 的倍数)之后。使用 ClockRes sysinternals 工具表明,即使在下面的代码中调用 timeBeginPeriod(1)
之后 运行 时,机器的系统计时器分辨率仍为 15ms。
使用 https://cms.lucashale.com/timer-resolution/ 将分辨率设置为最大支持值 (0.5ms) 不会更改示例代码的行为。
据我所知,机器正在使用不变的 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: {gap}ms");
}, "", 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;
}
}
}
这似乎是 issue with windows 10 2004。我猜它与 processor/motherboard.
无关可能的解决方法是使用 stopwatch and spinwait on a thread。这对于常规消费者应用程序是不可取的,因为它会消耗一个完整的线程,但如果您对系统有完全控制权,这可能是可行的。
运行同样的问题,我用的是CreateTimerQueueTimer
。仍然有效的是 timeSetEvent
。你会失去一些精度,因为它是整毫秒,但总比没有好。
我在 Windows 10 2004 下遇到了完全相同的问题。以前的版本似乎没有表现出相同的行为。 CreateTimerQueueTimer 似乎不再支持 timeBeginPeriod,它的最小周期似乎是 15 毫秒(好旧的 15 毫秒...)。
周围有一些人抱怨这个问题,但不是很多。 (例如,参见 this forum entry。 我不知道这是 v2004 中引入的错误,还是我们偷偷摸摸的 power-saving“功能”。 话虽这么说,官方文档从未将 TimerQueueTimers 和 timeBeginPeriod 联系起来,所以如果一开始可能是一个错误,他们就会尊重 timeBeginPeriod 设置。
无论如何,我最终 re-implementing 在 timeBeginPeriod/timeSetEvent 之上添加了一个 TimerQueue 以达到所需的计时器频率。