我开发了一个.NET应用程序,必须以120 Hz的频率轮询压力设备。标准的.NET计时器似乎只能达到60 Hz,因此我决定通过PInvoke使用Win32 CreateTimerQueueTimer API。它运行良好,但是调试体验非常糟糕,因为当程序处于暂停状态时,我正在逐步执行程序时,计时器甚至会被触发。我在C和C#中写了一个最小的例子,不希望的行为只发生在C#上。当调试器暂停程序时,C程序不会创建计时器回调线程。谁能告诉我,我能做些什么来在C#中实现相同的调试行为?
C代码:
#include <stdio.h>
#include <assert.h>
#include <Windows.h>
int counter = 0;
VOID NTAPI callback(PVOID lpParameter, BOOLEAN TimerOrWaitFired)
{
printf("Just in time %i\n", counter++);
}
int main()
{
HANDLE timer;
BOOL success = CreateTimerQueueTimer(&timer, NULL, callback, NULL, 0, 1000, WT_EXECUTEDEFAULT);
assert(FALSE != success); // set breakpoint at this line and wait 10 seconds
Sleep(1000);
success = DeleteTimerQueueTimer(NULL, timer, NULL); // step to this line
assert(FALSE != success);
return 0;
}
等效的C#代码:
using System;
using System.Runtime.InteropServices;
class TimerQueue
{
delegate void WAITORTIMERCALLBACK(IntPtr lpParameter, bool TimerOrWaitFired);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CreateTimerQueueTimer(
out IntPtr phNewTimer,
IntPtr TimerQueue,
WAITORTIMERCALLBACK Callback,
IntPtr Parameter,
uint DueTime,
uint Period,
uint Flags);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeleteTimerQueueTimer(
IntPtr TimerQueue,
IntPtr Timer,
IntPtr CompletionEvent);
static int counter = 0;
static void Callback(IntPtr lpParameter, bool TimerOrWaitFired)
{
Console.WriteLine("Just in time {0}", counter++);
}
static void Main(string[] args)
{
WAITORTIMERCALLBACK callbackWrapper = Callback;
IntPtr timer;
bool success = CreateTimerQueueTimer(out timer, IntPtr.Zero, callbackWrapper, IntPtr.Zero, 0, 1000, 0);
System.Diagnostics.Debug.Assert(false != success); // set breakpoint at this line and wait 10 seconds
System.Threading.Thread.Sleep(1000);
success = DeleteTimerQueueTimer(IntPtr.Zero, timer, IntPtr.Zero); // step to this line
System.Diagnostics.Debug.Assert(false != success);
}
}
顺便说一句,我知道在使用多线程的不受保护的计数器变量时存在竞争条件,这现在并不重要。
一秒钟的休眠意味着在断点被击中后无需等待,并且似乎是必要的,因为即使在调试器中步进程序但是仅在短暂的延迟之后,回调也不会立即在进程中排队。
对DeleteTimerQueueTimer的调用对于显示我的问题并不是必需的,因为它发生在执行此行之前。
答案 0 :(得分:1)
我看到我 4 年前问过这个问题,现在我想我知道答案了:.NET 中的计时器类在内部只使用一次性计时器。因此,当您在断点处暂停程序时,重新设置一次性计时器的代码不会运行,因此在您让程序/调试器继续运行之前,不会有回调堆积。
几个月前,我编写了自己的 Timer 类,该类可以达到高达 1000 Hz 的回调频率。它使用重复设置一次性定时器和忙等待的组合。忙等待会消耗 CPU 周期,但有助于减少 Windows 线程调度程序引入的抖动。
代码如下:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
/// <summary>
/// Executes a function periodically.
/// Timer expirations are not queued during debug breaks or
/// if the function takes longer than the period.
/// </summary>
/// <remarks>
/// Uses a Windows one-shot timer together
/// with busy waiting to achieve good accuracy
/// at reasonable CPU usage.
/// Works even on Windows 10 Version 2004.
/// </remarks>
public sealed class Timer : IDisposable
{
/// <summary>
/// Statistics:
/// How much time was 'wasted' for busy waiting.
/// Does not include time passed by using a system timer.
/// </summary>
public TimeSpan TimeSpentWithBusyWaiting => new TimeSpan(Interlocked.Read(ref timeSpentWithBusyWaiting));
private long timeSpentWithBusyWaiting = 0;
private readonly TimeCaps timeCaps;
private readonly Func<bool> function;
private readonly TimeSpan period;
/// <summary>
/// We must keep a reference to this callback so that it does not get garbage collected.
/// </summary>
private readonly TIMECALLBACK callback;
private readonly Stopwatch stopwatch = new Stopwatch();
private volatile bool stopTimer = false;
private ManualResetEvent timerStopped = new ManualResetEvent(false);
private TimeSpan desiredNextTime;
/// <summary>
/// Immediately starts the timer.
/// </summary>
/// <param name="function">
/// What to do after each <paramref name="period"/>.
/// The timer will stop if the function returns <see langword="false"/>.
/// </param>
/// <param name="period">
/// How long to wait between executing each <paramref name="function"/>.
/// </param>
public Timer(Func<bool> function, TimeSpan period)
{
uint? timerDelay = TimeSpanToPositiveMillisecondsWithRoundingDown(period);
if (timerDelay == null)
{
throw new ArgumentOutOfRangeException(nameof(period));
}
uint timeGetDevCapsErrorCode = TimeGetDevCaps(out timeCaps, (uint)Marshal.SizeOf(typeof(TimeCaps)));
if (timeGetDevCapsErrorCode != 0)
{
throw new Exception($"{nameof(TimeGetDevCaps)} returned error code {timeGetDevCapsErrorCode}");
}
Debug.Assert(timeCaps.wPeriodMin >= 1);
this.function = function ?? throw new ArgumentNullException(nameof(function));
this.period = period;
callback = new TIMECALLBACK(OnTimerExpired);
TimeBeginPeriod(timeCaps.wPeriodMin);
stopwatch.Start();
Schedule(desiredNextTime = period, forceTimer: true);
}
/// <summary>
/// Does not cancel the timer and instead
/// blocks until the function passed to
/// the constructor returns <see langword="false"/>.
/// </summary>
public void WaitUntilFinished()
{
if (timerStopped != null)
{
timerStopped.WaitOne();
timerStopped.Dispose();
timerStopped = null;
TimeEndPeriod(timeCaps.wPeriodMin);
}
}
/// <summary>
/// Stops timer and blocks until the
/// last invocation of the function has finished.
/// </summary>
public void Dispose()
{
stopTimer = true;
WaitUntilFinished();
}
private void OnTimerExpired(
uint uTimerID,
uint uMsg,
UIntPtr dwUser,
UIntPtr dw1,
UIntPtr dw2)
{
while (!stopTimer)
{
TimeSpan startOfBusyWaiting = stopwatch.Elapsed;
TimeSpan endOfBusyWaiting = desiredNextTime;
TimeSpan timeThatWillBeSpentWithBusyWaiting = endOfBusyWaiting - startOfBusyWaiting;
if (timeThatWillBeSpentWithBusyWaiting > TimeSpan.Zero)
{
Interlocked.Add(ref timeSpentWithBusyWaiting, timeThatWillBeSpentWithBusyWaiting.Ticks);
}
if (desiredNextTime > stopwatch.Elapsed)
{
while (desiredNextTime > stopwatch.Elapsed)
{
// busy waiting until time has arrived
}
desiredNextTime += period;
}
else
{
// we are too slow
desiredNextTime = stopwatch.Elapsed + period;
}
bool continueTimer = function();
if (continueTimer)
{
if (Schedule(desiredNextTime, forceTimer: false))
{
return;
}
}
else
{
stopTimer = true;
}
}
timerStopped.Set();
}
/// <param name="desiredNextTime">
/// Desired absolute time for next execution of function.
/// </param>
/// <param name="forceTimer">
/// If <see langword="true"/>, a one-shot timer will be used even if
/// <paramref name="desiredNextTime"/> is in the past or too close to
/// the system timer resolution.
/// </param>
/// <returns>
/// <see langword="true"/> if timer was set or <paramref name="forceTimer"/> was <see langword="true"/>.
/// <see langword="false"/> if <paramref name="desiredNextTime"/> was in the past.
/// </returns>
/// <remarks>
/// Increases accuracy by scheduling the timer a little earlier and
/// then do busy waiting outside of this function.
/// </remarks>
private bool Schedule(TimeSpan desiredNextTime, bool forceTimer)
{
TimeSpan currentTime = stopwatch.Elapsed;
TimeSpan remainingTimeUntilNextExecution = desiredNextTime - currentTime;
uint? timerDelay = TimeSpanToPositiveMillisecondsWithRoundingDown(remainingTimeUntilNextExecution);
timerDelay =
timerDelay < timeCaps.wPeriodMin ? timeCaps.wPeriodMin :
timerDelay > timeCaps.wPeriodMax ? timeCaps.wPeriodMax :
timerDelay;
if (forceTimer && timerDelay == null)
{
timerDelay = timeCaps.wPeriodMin;
}
if (forceTimer || timerDelay >= timeCaps.wPeriodMin)
{
// wait until next execution using a one-shot timer
uint timerHandle = TimeSetEvent(
timerDelay.Value,
timeCaps.wPeriodMin,
callback,
UIntPtr.Zero,
0);
if (timerHandle == 0)
{
throw new Exception($"{nameof(TimeSetEvent)} failed");
}
return true;
}
else // use busy waiting
{
return false;
}
}
/// <returns><see langword="null"/> if <paramref name="timeSpan"/> is negative</returns>
private static uint? TimeSpanToPositiveMillisecondsWithRoundingDown(TimeSpan timeSpan)
{
if (timeSpan.Ticks >= 0)
{
long milliseconds = timeSpan.Ticks / TimeSpan.TicksPerMillisecond;
if (milliseconds <= uint.MaxValue)
{
return unchecked((uint)milliseconds);
}
}
return null;
}
private delegate void TIMECALLBACK(
uint uTimerID,
uint uMsg,
UIntPtr dwUser,
UIntPtr dw1,
UIntPtr dw2);
// https://docs.microsoft.com/en-us/previous-versions//dd757634(v=vs.85)
//
// This is the only timer API that seems to work for frequencies
// higher than 60 Hz on Windows 10 Version 2004.
//
// The uResolution parameter has the same effect as
// using the timeBeginPeriod API, so it can be observed
// by entering `powercfg.exe /energy /duration 1` into
// a command prompt with administrator privileges.
[DllImport("winmm", EntryPoint = "timeSetEvent")]
private static extern uint TimeSetEvent(
uint uDelay,
uint uResolution,
TIMECALLBACK lpTimeProc,
UIntPtr dwUser,
uint fuEvent);
[DllImport("winmm", EntryPoint = "timeBeginPeriod")]
private static extern uint TimeBeginPeriod(
uint uPeriod);
[DllImport("winmm", EntryPoint = "timeEndPeriod")]
private static extern uint TimeEndPeriod(
uint uPeriod);
[DllImport("winmm", EntryPoint = "timeGetDevCaps")]
private static extern uint TimeGetDevCaps(
out TimeCaps ptc,
uint cbtc);
[StructLayout(LayoutKind.Sequential)]
private struct TimeCaps
{
public uint wPeriodMin;
public uint wPeriodMax;
}
}
答案 1 :(得分:0)
您应该能够进入Debug-&gt; Windows-&gt; Threads窗口并在步进时冻结所有线程。
答案 2 :(得分:-1)
由于C可以直接访问系统(kernel32)的功能,与C#相比,它更直接,安全性更低。 C#是在运行时编译的,所有调用都是外部世界(DLLs / COM),它必须使用OS提供的Wrapper(Windows)。
正如PhillipH所写:在断点期间检查“线程”窗口。在C#代码中,您将看到有RunParkingWindow
和.NET SystemEvent
等主题。
与C程序相比,只有一个线程 - 您刚刚停止调试的线程。
实现这一点您将理解,调试中的断点只会停止您当前所在的线程,但其他线程将继续运行。这也意味着Wrappers provided by OS
仍在侦听任何事件,只是将它们设置在队列中。一旦你的应用程序继续运行,他们就会把所有可能的东西扔向它,而Callback方法将完成其余的工作。
我试图找到任何有关数据流的相关文章/图片,但我没有成功。请将上述文本视为一个意见而非技术段落。