Windows计时器队列计时器在.NET断点

时间:2017-01-19 13:03:58

标签: c# debugging winapi timer breakpoints

我开发了一个.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 result

等效的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);
    }
}

C# result

顺便说一句,我知道在使用多线程的不受保护的计数器变量时存在竞争条件,这现在并不重要。

一秒钟的休眠意味着在断点被击中后无需等待,并且似乎是必要的,因为即使在调试器中步进程序但是仅在短暂的延迟之后,回调也不会立即在进程中排队。

对DeleteTimerQueueTimer的调用对于显示我的问题并不是必需的,因为它发生在执行此行之前。

3 个答案:

答案 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方法将完成其余的工作。

我试图找到任何有关数据流的相关文章/图片,但我没有成功。请将上述文本视为一个意见而非技术段落。