System.Timers.Timer 每秒最多只能提供 64 帧
Posted
技术标签:
【中文标题】System.Timers.Timer 每秒最多只能提供 64 帧【英文标题】:System.Timers.Timer only gives maximum 64 frames per second 【发布时间】:2012-11-22 23:54:17 【问题描述】:我有一个应用程序,它使用 System.Timers.Timer 对象来引发由主窗体(Windows Forms,C#)处理的事件。我的问题是,无论我将 .Interval 设置多短(甚至设置为 1 毫秒),我每秒最多只能执行 64 次。
我知道 Forms 计时器有 55 毫秒的精度限制,但这是 System.Timer 的变体,而不是 Forms 的变体。
应用程序占用 1% 的 CPU,因此它绝对不受 CPU 限制。所以它所做的就是:
将计时器设置为 1&nsp;ms 当事件触发时,增加一个 _Count 变量 再次将其设置为 1&nsp;ms 并重复_Count 最多每秒增加 64 次,即使没有其他工作可做。
这是一个“回放”应用程序,它必须复制传入的数据包,它们之间的延迟只有 1-2 毫秒,所以我需要能够可靠地每秒触发 1000 次左右的东西(尽管我会满足于100 如果我受 CPU 限制,我不是)。
有什么想法吗?
【问题讨论】:
顺便说一句,我删除了事件处理程序和主窗体之间的所有通信(仅用于测试),以确保我没有卡在某些消息队列中。没有区别。 试试System.Threading.Timer
看看它是否有效。
恕我直言,这不是最好的方法。尝试全速运行并使用延迟来减速。
55 毫秒是旧的 MS-Dos 时钟中断率。 Windows 每秒有 64 个中断。 timeBeginPeriod 来改变它。
所有 .NET 计时器都有这个限制 - System.Timers.Timer
、System.Threading.Timer
和 Windows.Forms.Timer
;见this question
【参考方案1】:
试试Multimedia Timers - 它们为硬件平台提供尽可能高的准确性。这些计时器以比其他计时器服务更高的分辨率安排事件。
您将需要以下 Win API 函数来设置计时器分辨率、启动和停止计时器:
[DllImport("winmm.dll")]
private static extern int timeGetDevCaps(ref TimerCaps caps, int sizeOfTimerCaps);
[DllImport("winmm.dll")]
private static extern int timeSetEvent(int delay, int resolution, TimeProc proc, int user, int mode);
[DllImport("winmm.dll")]
private static extern int timeKillEvent(int id);
你还需要回调委托:
delegate void TimeProc(int id, int msg, int user, int param1, int param2);
和定时器功能结构
[StructLayout(LayoutKind.Sequential)]
public struct TimerCaps
public int periodMin;
public int periodMax;
用法:
TimerCaps caps = new TimerCaps();
// provides min and max period
timeGetDevCaps(ref caps, Marshal.SizeOf(caps));
int period = 1;
int resolution = 1;
int mode = 0; // 0 for periodic, 1 for single event
timeSetEvent(period, resolution, new TimeProc(TimerCallback), 0, mode);
还有回调:
void TimerCallback(int id, int msg, int user, int param1, int param2)
// occurs every 1 ms
【讨论】:
与标准计时器调用相比,更好的粒度并不是 timeSetEvent 的一个特性。使用此处的代码,系统中断周期将更改为以 1 ms 运行。然而,当它被使用时,所有的定时器函数都将有 1 毫秒的粒度。因此,您只需将set the timer resolution 设置为最大(最小周期)即可让所有计时器在该级别运行。 仅供参考,多媒体计时器不是最准确的 - 高性能计时器是。我使用了以 1 kHz 滴答作响的多媒体计时器,有时您会遇到一个超时(对控制回路不利)。与多媒体计时器相比,HPT 是“非常稳定”的。 @HansPassantThreading.Timer
不在乎你如何设置 timeBeginPeriod。 It continues to ticks at 15.6ms intervals even when timeBeginPeriod is set to 1ms.【参考方案2】:
您可以坚持您的设计。您只需将系统中断频率设置为以其最大频率运行。为了获得这个,你只需要在代码中的任何地方执行以下代码:
#define TARGET_RESOLUTION 1 // 1-millisecond target resolution
TIMECAPS tc;
UINT wTimerRes;
if (timeGetDevCaps(&tc, sizeof(TIMECAPS)) != TIMERR_NOERROR)
// Error; application can't continue.
wTimerRes = min(max(tc.wPeriodMin, TARGET_RESOLUTION), tc.wPeriodMax);
timeBeginPeriod(wTimerRes);
这将强制系统中断周期以最大频率运行。这是一个系统范围的行为,因此它甚至可以在一个单独的进程中完成。不要忘记使用
MMRESULT timeEndPeriod(wTimerRes );
完成后释放资源并将中断周期重置为默认值。详情请见Multimedia Timers。
您必须将每个对timeBeginPeriod
的调用与对timeEndPeriod
的调用相匹配,并在两个调用中指定相同的最小分辨率。只要每个调用与对timeEndPeriod
的调用匹配,应用程序就可以进行多次timeBeginPeriod
调用。
因此,所有计时器(包括您当前的设计)都将以更高的频率运行,因为计时器的粒度将会提高。在大多数硬件上都可以获得 1 毫秒的粒度。
以下是使用wTimerRes
的各种设置获得的中断周期列表,用于两种不同的硬件设置 (A+B):
很容易看出,1 ms 是一个理论值。 ActualResolution 以 100 ns 为单位。 9,766 表示 0.9766 毫秒,即每秒 1024 次中断。 (实际上它应该是 0.9765625,即 9,7656.25 100 ns 单位,但该精度显然不适合整数,因此会被系统四舍五入。)
很明显,i.g.平台 A 并不真正支持 timeGetDevCaps
返回的所有周期范围(值介于 wPeriodMin
和 wPeriodMin
之间)。
总结:多媒体定时器接口可用于修改中断频率系统范围。因此,所有计时器都会改变它们的粒度。系统时间更新也会相应地改变,它会更频繁地以更小的步长增加。 但是:实际行为取决于底层硬件。自引入 Windows 7 和 Windows 8 以来,这种硬件依赖性已经小很多,因为引入了更新的计时方案。
【讨论】:
只是硬编码 timeBeginPeriod(1),没有任何 Windows 机器不支持它。 @HansPassant:可以尝试一下,但为了完美主义而讲述了完整的故事。但timeEndPeriod
不应被遗忘。
我添加了代码并单步验证我现在正在调用值为 1 的 timeBeginPeriod,并且它返回 TIMERR_NOERROR。但是,我仍然看到每秒 64 次中断。需要明确的是,timeBeginPeriod 会影响 System.Timers.Timer 吗?无论出于何种原因,它似乎成功但没有效果。
@Dave:有趣的结果。 timeGetDevCaps
是否返回了 1 的 .PeriodMin
?请继续检查中断周期是否已更改。只需在循环中使用GetSystemTimeAsFileTime(...)
来测试system file time
增量。请参阅this SO 答案以获取代码和描述。当每秒仅发生 64 次中断时,系统文件时间增量将为 15.625 毫秒。
系统中断频率?这不需要管理权限来更改吗?【参考方案3】:
基于其他解决方案和 cmets,我将这段 VB.NET 代码放在一起。可以用表格粘贴到项目中。我理解@HansPassant 的cmets 说,只要调用timeBeginPeriod
,“常规计时器也会变得准确”。在我的代码中似乎不是这种情况。
在使用timeBeginPeriod
将计时器分辨率设置为最小值后,我的代码创建了一个多媒体计时器、一个System.Threading.Timer
、一个System.Timers.Timer
和一个Windows.Forms.Timer
。多媒体定时器按要求以 1 kHz 运行,但其他定时器仍停留在 64 Hz。所以要么我做错了,要么无法更改内置 .NET 计时器的分辨率。
编辑;更改代码以使用 StopWatch 类进行计时。
Imports System.Runtime.InteropServices
Public Class Form1
'From http://www.pinvoke.net/default.aspx/winmm/MMRESULT.html
Private Enum MMRESULT
MMSYSERR_NOERROR = 0
MMSYSERR_ERROR = 1
MMSYSERR_BADDEVICEID = 2
MMSYSERR_NOTENABLED = 3
MMSYSERR_ALLOCATED = 4
MMSYSERR_INVALHANDLE = 5
MMSYSERR_NODRIVER = 6
MMSYSERR_NOMEM = 7
MMSYSERR_NOTSUPPORTED = 8
MMSYSERR_BADERRNUM = 9
MMSYSERR_INVALFLAG = 10
MMSYSERR_INVALPARAM = 11
MMSYSERR_HANDLEBUSY = 12
MMSYSERR_INVALIDALIAS = 13
MMSYSERR_BADDB = 14
MMSYSERR_KEYNOTFOUND = 15
MMSYSERR_READERROR = 16
MMSYSERR_WRITEERROR = 17
MMSYSERR_DELETEERROR = 18
MMSYSERR_VALNOTFOUND = 19
MMSYSERR_NODRIVERCB = 20
WAVERR_BADFORMAT = 32
WAVERR_STILLPLAYING = 33
WAVERR_UNPREPARED = 34
End Enum
'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757625(v=vs.85).aspx
<StructLayout(LayoutKind.Sequential)>
Public Structure TIMECAPS
Public periodMin As UInteger
Public periodMax As UInteger
End Structure
'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757627(v=vs.85).aspx
<DllImport("winmm.dll")>
Private Shared Function timeGetDevCaps(ByRef ptc As TIMECAPS, ByVal cbtc As UInteger) As MMRESULT
End Function
'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757624(v=vs.85).aspx
<DllImport("winmm.dll")>
Private Shared Function timeBeginPeriod(ByVal uPeriod As UInteger) As MMRESULT
End Function
'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757626(v=vs.85).aspx
<DllImport("winmm.dll")>
Private Shared Function timeEndPeriod(ByVal uPeriod As UInteger) As MMRESULT
End Function
'http://msdn.microsoft.com/en-us/library/windows/desktop/ff728861(v=vs.85).aspx
Private Delegate Sub TIMECALLBACK(ByVal uTimerID As UInteger, _
ByVal uMsg As UInteger, _
ByVal dwUser As IntPtr, _
ByVal dw1 As IntPtr, _
ByVal dw2 As IntPtr)
'Straight from C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include\MMSystem.h
'fuEvent below is a combination of these flags.
Private Const TIME_ONESHOT As UInteger = 0
Private Const TIME_PERIODIC As UInteger = 1
Private Const TIME_CALLBACK_FUNCTION As UInteger = 0
Private Const TIME_CALLBACK_EVENT_SET As UInteger = &H10
Private Const TIME_CALLBACK_EVENT_PULSE As UInteger = &H20
Private Const TIME_KILL_SYNCHRONOUS As UInteger = &H100
'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757634(v=vs.85).aspx
'Documentation is self-contradicting. The return value is Uinteger, I'm guessing.
'"Returns an identifier for the timer event if successful or an error otherwise.
'This function returns NULL if it fails and the timer event was not created."
<DllImport("winmm.dll")>
Private Shared Function timeSetEvent(ByVal uDelay As UInteger, _
ByVal uResolution As UInteger, _
ByVal TimeProc As TIMECALLBACK, _
ByVal dwUser As IntPtr, _
ByVal fuEvent As UInteger) As UInteger
End Function
'http://msdn.microsoft.com/en-us/library/windows/desktop/dd757630(v=vs.85).aspx
<DllImport("winmm.dll")>
Private Shared Function timeKillEvent(ByVal uTimerID As UInteger) As MMRESULT
End Function
Private lblRate As New Windows.Forms.Label
Private WithEvents tmrUI As New Windows.Forms.Timer
Private WithEvents tmrWorkThreading As New System.Threading.Timer(AddressOf TimerTick)
Private WithEvents tmrWorkTimers As New System.Timers.Timer
Private WithEvents tmrWorkForm As New Windows.Forms.Timer
Public Sub New()
lblRate.AutoSize = True
Me.Controls.Add(lblRate)
InitializeComponent()
End Sub
Private Capability As New TIMECAPS
Private Sub Form1_FormClosing(sender As Object, e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
timeKillEvent(dwUser)
timeEndPeriod(Capability.periodMin)
End Sub
Private dwUser As UInteger = 0
Private Clock As New System.Diagnostics.Stopwatch
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) _
Handles MyBase.Load
Dim Result As MMRESULT
'Get the min and max period
Result = timeGetDevCaps(Capability, Marshal.SizeOf(Capability))
If Result <> MMRESULT.MMSYSERR_NOERROR Then
MsgBox("timeGetDevCaps returned " + Result.ToString)
Exit Sub
End If
'Set to the minimum period.
Result = timeBeginPeriod(Capability.periodMin)
If Result <> MMRESULT.MMSYSERR_NOERROR Then
MsgBox("timeBeginPeriod returned " + Result.ToString)
Exit Sub
End If
Clock.Start()
Dim uTimerID As UInteger
uTimerID = timeSetEvent(Capability.periodMin, Capability.periodMin, _
New TIMECALLBACK(AddressOf MMCallBack), dwUser, _
TIME_PERIODIC Or TIME_CALLBACK_FUNCTION Or TIME_KILL_SYNCHRONOUS)
If uTimerID = 0 Then
MsgBox("timeSetEvent not successful.")
Exit Sub
End If
tmrWorkThreading.Change(0, 1)
tmrWorkTimers.Interval = 1
tmrWorkTimers.Enabled = True
tmrWorkForm.Interval = 1
tmrWorkForm.Enabled = True
tmrUI.Interval = 100
tmrUI.Enabled = True
End Sub
Private CounterThreading As Integer = 0
Private CounterTimers As Integer = 0
Private CounterForms As Integer = 0
Private CounterMM As Integer = 0
Private ReadOnly TimersLock As New Object
Private Sub tmrWorkTimers_Elapsed(sender As Object, e As System.Timers.ElapsedEventArgs) _
Handles tmrWorkTimers.Elapsed
SyncLock TimersLock
CounterTimers += 1
End SyncLock
End Sub
Private ReadOnly ThreadingLock As New Object
Private Sub TimerTick()
SyncLock ThreadingLock
CounterThreading += 1
End SyncLock
End Sub
Private ReadOnly MMLock As New Object
Private Sub MMCallBack(ByVal uTimerID As UInteger, _
ByVal uMsg As UInteger, _
ByVal dwUser As IntPtr, _
ByVal dw1 As IntPtr, _
ByVal dw2 As IntPtr)
SyncLock MMLock
CounterMM += 1
End SyncLock
End Sub
Private ReadOnly FormLock As New Object
Private Sub tmrWorkForm_Tick(sender As Object, e As System.EventArgs) Handles tmrWorkForm.Tick
SyncLock FormLock
CounterForms += 1
End SyncLock
End Sub
Private Sub tmrUI_Tick(sender As Object, e As System.EventArgs) _
Handles tmrUI.Tick
Dim Secs As Integer = Clock.Elapsed.TotalSeconds
If Secs > 0 Then
Dim TheText As String = ""
TheText += "System.Threading.Timer " + (CounterThreading / Secs).ToString("#,##0.0") + "Hz" + vbCrLf
TheText += "System.Timers.Timer " + (CounterTimers / Secs).ToString("#,##0.0") + "Hz" + vbCrLf
TheText += "Windows.Forms.Timer " + (CounterForms / Secs).ToString("#,##0.0") + "Hz" + vbCrLf
TheText += "Multimedia Timer " + (CounterMM / Secs).ToString("#,##0.0") + "Hz"
lblRate.Text = TheText
End If
End Sub
End Class
【讨论】:
以上是关于System.Timers.Timer 每秒最多只能提供 64 帧的主要内容,如果未能解决你的问题,请参考以下文章
为啥 System.Timers.Timer 能在 GC 中存活,而 System.Threading.Timer 不能?
使用System.Timers.Timer类实现程序定时执行
System.Windows.Forms.TimerSystem.Timers.TimerSystem.Threading.Timer