System.Timers.Timer只给出每秒最大64帧(System.Timers.Timer

2019-07-04 01:56发布

我有一个使用System.Timers.Timer对象引发了由主要形式处理的事件(应用程序Windows窗体 ,C#)。 我的问题是,无论我有多短设置.Interval(甚至1毫秒),我得到的每秒64次最大。

我知道窗体定时器具有55毫秒的精度限制,但这是System.Timer变种,而不是形式之一。

该应用程序位于1%的CPU,所以它绝对不是CPU绑定。 因此,所有它做的是:

  • 设置定时器以1 NSP; MS
  • 当事件触发,增加一个_COUNT变量
  • 再次,重复毫秒;它设置为1和NSP

即使没有其他工作要做_COUNT得到增加最多64次。

这是一个“回放”应用程序,它具有复制数据包来与他们之间低至1-2毫秒的延迟,所以我需要的东西,可以可靠地触发1000次一秒钟左右(虽然我如果我满足于100在CPU束缚,我不是)。

有什么想法吗?

Answer 1:

尝试多媒体计时器 -它们提供了硬件平台最大的准确度可能。 这些定时器安排在比其他计时器服务更高的分辨率事件。

您将需要以下赢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
}


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

在完成时释放资源并重置中断周期为默认。 见多媒体计时器的详细信息。

你必须每次调用匹配timeBeginPeriod有一个呼叫timeEndPeriod ,指定在这两个电话的同一最小分辨率。 应用程序可以使多个timeBeginPeriod为每个呼叫与一个呼叫电话相匹配,只要timeEndPeriod

因此,所有的定时器(包括当前的设计),将在更高的频率下运行,因为计时器的粒度将得到改善。 可以在大多数硬件来获得1毫秒的粒度。

下面是各种设置获得中断周期列表wTimerRes用于两个不同的硬件设置(A + B):

它可以很容易地看出,1毫秒是理论值。 ActualResolution以100周纳秒为单位给出。 9766表示0.9766毫秒这是每秒1024个中断。 (事实上​​,它应该是0.9765625这将是9,7656.25 100ns的单位,但准确性明显不适合的整数,并因此由系统四舍五入)。

它也变得明显,IG平台并没有真正支持恢复周期的所有范围timeGetDevCaps (值之间不等wPeriodMinwPeriodMin )。

总结:多媒体计时器接口可用于广泛修改中断频率系统 。 因此所有的计时器会改变他们的粒度。 另外,系统时间更新将发生相应的变化,它会增加更经常地在更小的步骤。 但是:实际行为取决于在垫层硬件。 这种硬件的依赖已经变得自从为定时更新方案已经推出了介绍Windows 7和Windows 8的小了很多。



Answer 3:

基于其他解决方案和意见,我整理了这篇VB.NET代码。 可以粘贴到与窗体的项目。 我明白@ HansPassant的评论的话说,只要timeBeginPeriod被调用时,“定期计时器得到准确的一样。” 这似乎并不在我的代码的情况。

我的代码创建一个多媒体计时器, System.Threading.Timer ,一个System.Timers.Timer ,和Windows.Forms.Timer使用后timeBeginPeriod设置计时器分辨率到最小。 多媒体计时器在1 kHz运行的要求,但其他人仍然在64赫兹卡住。 因此,无论我做错了什么,或者有没有办法改变内置.NET计时器的分辨率。

编辑 ; 改变为使用秒表类用于定时的代码。

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 only gives maximum 64 frames per second