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, я не).
Любые мысли?
Ответы
Ответ 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