System.Timers.Timer只能提供每秒最多64帧。

12
我有一个应用程序,使用System.Timers.Timer对象触发事件,由主窗体(Windows Forms,C#)处理。我的问题是,无论我将.Interval设置多短(甚至为1毫秒),我最多只能每秒获得64次。
我知道Forms计时器有55毫秒的精度限制,但这是System.Timer变量,而不是Forms计时器。
应用程序占用1%的CPU,因此绝对不会受到CPU限制。所以它所做的就是:
- 将计时器设置为1毫秒 - 当事件触发时,增加_Count变量 - 再次将其设置为1毫秒并重复
即使没有其他工作要做,_Count也最多每秒增加64次。
这是一个“回放”应用程序,必须在它们之间尽可能少地延迟1-2毫秒来复制传入的数据包,因此我需要能够可靠地触发大约1000次/秒的东西(如果我受到CPU限制,我会满足于100次/秒,但我没有)。
有什么想法吗?

顺便说一下,我删除了事件处理程序和主窗体之间的所有通信(仅用于测试),以确保我没有被卡在某个消息队列中。但是这并没有产生任何影响。 - Dave
尝试使用 System.Threading.Timer 看看是否可行。 - Serdalis
在我看来,这不是最好的方法。尝试以全速运行并使用延迟来减速。 - Simon
1
旧版 MS-Dos 的时钟中断率为 55 毫秒。Windows 每秒钟会有 64 个中断。可以使用 timeBeginPeriod 来更改这个设置。 - Hans Passant
1
所有的.NET计时器都有这个限制 - System.Timers.TimerSystem.Threading.TimerWindows.Forms.Timer;请参见此问题 - darda
3个回答

4
尝试使用多媒体定时器 - 它们为硬件平台提供了可能的最高准确性。这些定时器以比其他计时器服务更高的分辨率安排事件。
您需要以下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
}

1
与标准计时器调用相比,timeSetEvent的粒度更好。在这里的代码中,系统的中断周期将被更改为以1毫秒运行。然而,当使用此功能时,所有计时器函数都将具有1毫秒的粒度。因此,您只需将计时器分辨率设置为最大值(最小周期),以使所有计时器在该级别上运行。 - Arno
1
FYI,多媒体定时器并不是最准确的 - 高性能定时器才是。我使用过1 kHz滴答的多媒体定时器,偶尔会有一个跑得很远(对于控制环路来说很糟糕)。与多媒体定时器相比,HPT非常稳定。 - darda
@HansPassant Threading.Timer 不关心你如何设置 timeBeginPeriod。即使将 timeBeginPeriod 设置为 1ms,它仍会以 15.6ms 的间隔继续进行滴答声。详见:https://dev59.com/UnHYa4cB1Zd3GeqPJkYe。 - Evgeniy Berezovsky

3

您可以按照自己的设计进行操作。您只需要将系统中断频率设置为最大频率即可。为了实现这一点,您只需要在代码的任何位置执行以下代码:

#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 );

完成后,需要释放资源并将中断周期重置为默认值。有关详细信息,请参见多媒体定时器
每次调用timeBeginPeriod必须与调用timeEndPeriod匹配,在两个调用中都指定相同的最小分辨率。只要每个调用都与一个调用timeEndPeriod匹配,应用程序就可以进行多个timeBeginPeriod调用。
因此,所有计时器(包括当前设计)将以更高的频率运行,因为计时器的粒度将提高。在大多数硬件上可以获得1毫秒的粒度。
以下是使用不同wTimerRes设置获得的两种不同硬件设置(A+B)的中断周期列表: ActualResolution (interrupt period) vs. setting of wTimerRes 可以很容易地看出1毫秒是一个理论值。ActualResolution以100纳秒为单位给出。9,766表示0.9766毫秒,即每秒1024个中断。(实际上应该是0.9765625,这将是97656.25 100纳秒单位,但这种准确性显然不能适应整数,因此被系统四舍五入。)
还很明显,例如平台A实际上并不支持timeGetDevCaps返回的所有周期范围(值在wPeriodMinwPeriodMin之间)。 总结:多媒体定时器界面可用于修改中断频率系统范围。因此,所有计时器都将改变其粒度。系统时间更新也会相应地更改,它将更频繁地增加并以更小的步长增加。但是,实际行为取决于底层硬件。自Windows 7和Windows 8引入新的定时方案以来,这种硬件依赖性已经大大减小。

只需硬编码timeBeginPeriod(1),没有任何Windows机器不支持它。 - Hans Passant
@HansPassant:可以尝试一下,但为了完美主义的缘故,我讲述了整个故事。但是timeEndPeriod也不应该被忘记。 - Arno
我添加了代码并进行了单步调试,以验证我现在使用值为1调用timeBeginPeriod,并且它返回TIMERR_NOERROR。然而,我仍然看到每秒64个中断。明确一下,timeBeginPeriod是否影响System.Timers.Timer?出于某种原因,它似乎成功了但没有效果。 - Dave
@Dave:有趣的结果。timeGetDevCaps返回了1的.PeriodMin吗?请继续检查中断周期是否已更改。只需使用循环测试GetSystemTimeAsFileTime(...)即可测试系统文件时间增量。请参见 SO答案以获取代码和说明。当每秒仅发生64个中断时,系统文件时间增量将为15.625毫秒。 - Arno
系统中断频率?这不需要管理员权限才能更改吗? - Peter Mortensen
@PeterMortensen:使用timeBeginPeriod函数,无需管理员权限即可实现。有关更多详细信息,请参见计时器分辨率 - Arno

0

基于其他解决方案和评论,我编写了这段VB.NET代码。可以将其粘贴到带有表单的项目中。我理解@HansPassant的评论是说只要调用timeBeginPeriod,“常规计时器也会变得准确”。但在我的代码中似乎并非如此。

我的代码创建了一个多媒体定时器、一个System.Threading.Timer、一个System.Timers.Timer和一个Windows.Forms.Timer,然后使用timeBeginPeriod将定时器分辨率设置为最小值。多媒体定时器按照所需的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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接