Подтвердить что ты не робот

System.Timers.Timer дает максимум 64 кадра в секунду

У меня есть приложение, которое использует объект System.Timers.Timer для создания событий, которые обрабатываются основной формой (Windows Forms, С#). Моя проблема заключается в том, что независимо от того, насколько короток я устанавливаю .Interval(даже до 1 ms), я получаю максимум 64 раза в секунду.

Я знаю, что таймер Forms имеет ограничение точности 55 & mbs; MS, но это вариант System.Timer, а не Forms one.

Приложение занимает 1% процессор, поэтому он определенно не связан с ЦП. Итак, все, что он делает, это:

  • Установите таймер на 1 & nsp; ms
  • Когда событие срабатывает, увеличьте переменную _Count
  • Установите его снова на 1 & nsp; ms и повторите

_Count увеличивается до 64 раз в секунду, даже если нет другой работы.

Это приложение "воспроизведения", которое должно реплицировать пакеты, входящие всего за 1-2 мкс задержка между ними, поэтому мне нужно что-то, что может надежно срабатывать 1000 раз в секунду или около того (хотя я бы согласился для 100, если я был связан с CPU, я не).

Любые мысли?

4b9b3361

Ответ 1

Попробуйте Мультимедийные таймеры - они обеспечивают максимальную точность для аппаратной платформы. Эти таймеры планируют события с более высоким разрешением, чем другие службы таймера.

Вам понадобятся следующие функции 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);

Вам также нужен делегат callback:

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
}

Ответ 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 );

когда сделано, чтобы освободить ресурс и reset период прерывания по умолчанию. Подробнее см. Мультимедийные таймеры.

Вы должны сопоставить каждый вызов с timeBeginPeriod с вызовом timeEndPeriod, указав одинаковое минимальное разрешение в обоих вызовах. Приложение может выполнять несколько вызовов timeBeginPeriod, пока каждый вызов сопоставляется с вызовом timeEndPeriod.

Как следствие, все таймеры (включая ваш текущий проект) будут работать на более высокой частоте, так как гранулярность таймеров улучшится. Гранулярность 1 мкс может быть получена на большинстве аппаратных средств.

Ниже приведен список периодов прерываний, полученных с различными настройками wTimerRes для двух разных аппаратных настроек (A + B):

ActualResolution (interrupt period) vs. setting of wTimerRes

Нетрудно видеть, что 1 ms - теоретическое значение. ActualResolution дается в единицах 100 единиц. 9,766 - 0,976 мс, что составляет 1024 прерывания в секунду. (На самом деле это должно быть 0,9765625, что будет 9 766,25 100 единиц nsp; ns, но эта точность, очевидно, не вписывается в целое число и поэтому округляется системой.)

Также становится очевидным, что, например, платформа A на самом деле не поддерживает весь диапазон периодов, возвращаемых timeGetDevCaps (значения находятся в диапазоне между wPeriodMin и wPeriodMin).

Сводка: Интерфейс мультимедийного таймера может использоваться для изменения частотной системы прерываний. Как следствие, все таймеры изменят свою гранулярность. Кроме того, изменение времени системы будет соответствующим образом изменяться, оно будет увеличиваться чаще и с меньшими шагами. Но:. Фактическое поведение зависит от подстилающего оборудования. Эта аппаратная зависимость стала намного меньше с момента появления Windows 7 и Windows 8 после появления новых схем синхронизации.

Ответ 3

Основываясь на других решениях и комментариях, я собрал этот код VB.NET. Может вставляться в проект с формой. Я понял комментарии @HansPassant, говоря, что до тех пор, пока вызывается timeBeginPeriod, "регулярные таймеры также становятся точными". Это не похоже на мой код.

Мой код создает таймер мультимедиа, System.Threading.Timer, a System.Timers.Timer и a Windows.Forms.Timer после использования timeBeginPeriod, чтобы установить минимальное разрешение таймера. Мультимедийный таймер работает при частоте 1 кГц, но остальные все еще застревают на частоте 64 Гц. Так что либо я делаю что-то неправильно, либо нет возможности изменить разрешение встроенных таймеров .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