C# 为什么定时器频率非常低?

发布于 2024-07-10 12:38:14 字数 1749 浏览 6 评论 0原文

System.Timers.TimerSystem.Threading.Timer 触发的时间间隔与请求的时间间隔有很大不同。 例如:

new System.Timers.Timer(1000d / 20);

生成一个每秒触发 16 次而不是 20 次的计时器。

为了确保事件处理程序过长不会产生副作用,我编写了这个小测试程序:

int[] frequencies = { 5, 10, 15, 20, 30, 50, 75, 100, 200, 500 };

// Test System.Timers.Timer
foreach (int frequency in frequencies)
{
    int count = 0;

    // Initialize timer
    System.Timers.Timer timer = new System.Timers.Timer(1000d / frequency);
    timer.Elapsed += delegate { Interlocked.Increment(ref count); };

    // Count for 10 seconds
    DateTime start = DateTime.Now;
    timer.Enabled = true;
    while (DateTime.Now < start + TimeSpan.FromSeconds(10))
        Thread.Sleep(10);
    timer.Enabled = false;

    // Calculate actual frequency
    Console.WriteLine(
        "Requested frequency: {0}\nActual frequency: {1}\n",
        frequency, count / 10d);
}

输出如下所示:

Requested: 5 Hz; 实际:4,8 Hz
要求:10 Hz; 实际:9,1 Hz
要求:15 Hz; 实际:12,7 Hz
要求:20 Hz; 实际:16 Hz
要求:30 Hz; 实际:21,3 Hz
要求:50 Hz; 实际:31,8 Hz
要求:75 Hz; 实际:63,9 Hz
要求:100 Hz; 实际:63,8 Hz
要求:200 Hz; 实际:63,9 Hz
要求:500 Hz; 实际:63.9 Hz

实际频率与要求频率的偏差最多为 36%。 (显然不能超过 64 Hz。)鉴于 Microsoft 推荐此计时器是因为它比 System.Windows.Forms.Timer 具有“更高的准确性”,这让我很困惑。

顺便说一句,这些不是随机偏差。 它们每次都是相同的值。 另一个计时器类 System.Threading.Timer 的类似测试程序显示了完全相同的结果。

在我的实际程序中,我需要以每秒精确 50 个样本的速度收集测量结果。 这应该还不需要实时系统。 每秒获取 32 个样本而不是 50 个样本,这非常令人沮丧。

有什么想法吗?

@克里斯: 你是对的,这些间隔似乎都是 1/64 秒左右的整数倍。 顺便说一句,在事件处理程序中添加 Thread.Sleep(...) 没有任何区别。 鉴于 System.Threading.Timer 使用线程池,因此每个事件都在空闲线程上触发,这是有道理的。

Both System.Timers.Timer and System.Threading.Timer fire at intervals that are considerable different from the requested ones.
For example:

new System.Timers.Timer(1000d / 20);

yields a timer that fires 16 times per second, not 20.

To be sure that there are no side-effects from too long event handlers, I wrote this little test program:

int[] frequencies = { 5, 10, 15, 20, 30, 50, 75, 100, 200, 500 };

// Test System.Timers.Timer
foreach (int frequency in frequencies)
{
    int count = 0;

    // Initialize timer
    System.Timers.Timer timer = new System.Timers.Timer(1000d / frequency);
    timer.Elapsed += delegate { Interlocked.Increment(ref count); };

    // Count for 10 seconds
    DateTime start = DateTime.Now;
    timer.Enabled = true;
    while (DateTime.Now < start + TimeSpan.FromSeconds(10))
        Thread.Sleep(10);
    timer.Enabled = false;

    // Calculate actual frequency
    Console.WriteLine(
        "Requested frequency: {0}\nActual frequency: {1}\n",
        frequency, count / 10d);
}

The output looks like this:

Requested: 5 Hz; actual: 4,8 Hz
Requested: 10 Hz; actual: 9,1 Hz
Requested: 15 Hz; actual: 12,7 Hz
Requested: 20 Hz; actual: 16 Hz
Requested: 30 Hz; actual: 21,3 Hz
Requested: 50 Hz; actual: 31,8 Hz
Requested: 75 Hz; actual: 63,9 Hz
Requested: 100 Hz; actual: 63,8 Hz
Requested: 200 Hz; actual: 63,9 Hz
Requested: 500 Hz; actual: 63,9 Hz

The actual frequency deviates by up to 36% from the requested one. (And evidently cannot exceed 64 Hz.) Given that Microsoft recommends this timer for its "greater accuracy" over System.Windows.Forms.Timer, this puzzles me.

Btw, these are not random deviations. They are the same values every time.
And a similar test program for the other timer class, System.Threading.Timer, shows the exact same results.

In my actual program, I need to collect measurements at precisely 50 samples per second. This should not yet require a real-time system. And it is very frustrating to get 32 samples per second instead of 50.

Any ideas?

@Chris:
You are right, the intervals all seem to be integer multiples of something around 1/64th second. Btw, adding a Thread.Sleep(...) in the event handler doesn't make any difference. This makes sense given that System.Threading.Timer uses the thread pool, so each event is fired on a free thread.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

谢绝鈎搭 2024-07-17 12:38:14

如果您使用 winmm.dll,您可以使用更多的 CPU 时间,但可以更好地控制。

这是修改为使用 winmm.dll 计时器的示例

const String WINMM = "winmm.dll";
const String KERNEL32 = "kernel32.dll";

delegate void MMTimerProc (UInt32 timerid, UInt32 msg, IntPtr user, UInt32 dw1, UInt32 dw2);

[DllImport(WINMM)]
static extern uint timeSetEvent(
      UInt32            uDelay,      
      UInt32            uResolution, 
      [MarshalAs(UnmanagedType.FunctionPtr)] MMTimerProc lpTimeProc,  
      UInt32            dwUser,      
      Int32             fuEvent      
    );

[DllImport(WINMM)]
static extern uint timeKillEvent(uint uTimerID);

// Library used for more accurate timing
[DllImport(KERNEL32)]
static extern bool QueryPerformanceCounter(out long PerformanceCount);
[DllImport(KERNEL32)]
static extern bool QueryPerformanceFrequency(out long Frequency);

static long CPUFrequency;

static int count;

static void Main(string[] args)
{            
    QueryPerformanceFrequency(out CPUFrequency);

    int[] frequencies = { 5, 10, 15, 20, 30, 50, 75, 100, 200, 500 };

    foreach (int freq in frequencies)
    {
        count = 0;

        long start = GetTimestamp();

        // start timer
        uint timerId = timeSetEvent((uint)(1000 / freq), 0, new MMTimerProc(TimerFunction), 0, 1);

        // wait 10 seconds
        while (DeltaMilliseconds(start, GetTimestamp()) < 10000)
        {
            Thread.Sleep(1);
        }

        // end timer
        timeKillEvent(timerId);

        Console.WriteLine("Requested frequency: {0}\nActual frequency: {1}\n", freq, count / 10);
    }

    Console.ReadLine();
}

static void TimerFunction(UInt32 timerid, UInt32 msg, IntPtr user, UInt32 dw1, UInt32 dw2)
{
    Interlocked.Increment(ref count);
}

static public long DeltaMilliseconds(long earlyTimestamp, long lateTimestamp)
{
    return (((lateTimestamp - earlyTimestamp) * 1000) / CPUFrequency);
}

static public long GetTimestamp()
{
    long result;
    QueryPerformanceCounter(out result);
    return result;
}

这是我得到的输出:

Requested frequency: 5
Actual frequency: 5

Requested frequency: 10
Actual frequency: 10

Requested frequency: 15
Actual frequency: 15

Requested frequency: 20
Actual frequency: 19

Requested frequency: 30
Actual frequency: 30

Requested frequency: 50
Actual frequency: 50

Requested frequency: 75
Actual frequency: 76

Requested frequency: 100
Actual frequency: 100

Requested frequency: 200
Actual frequency: 200

Requested frequency: 500
Actual frequency: 500

希望这会有所帮助。

If you use winmm.dll you can use more CPU time, but have better control.

Here is your example modified to use the winmm.dll timers

const String WINMM = "winmm.dll";
const String KERNEL32 = "kernel32.dll";

delegate void MMTimerProc (UInt32 timerid, UInt32 msg, IntPtr user, UInt32 dw1, UInt32 dw2);

[DllImport(WINMM)]
static extern uint timeSetEvent(
      UInt32            uDelay,      
      UInt32            uResolution, 
      [MarshalAs(UnmanagedType.FunctionPtr)] MMTimerProc lpTimeProc,  
      UInt32            dwUser,      
      Int32             fuEvent      
    );

[DllImport(WINMM)]
static extern uint timeKillEvent(uint uTimerID);

// Library used for more accurate timing
[DllImport(KERNEL32)]
static extern bool QueryPerformanceCounter(out long PerformanceCount);
[DllImport(KERNEL32)]
static extern bool QueryPerformanceFrequency(out long Frequency);

static long CPUFrequency;

static int count;

static void Main(string[] args)
{            
    QueryPerformanceFrequency(out CPUFrequency);

    int[] frequencies = { 5, 10, 15, 20, 30, 50, 75, 100, 200, 500 };

    foreach (int freq in frequencies)
    {
        count = 0;

        long start = GetTimestamp();

        // start timer
        uint timerId = timeSetEvent((uint)(1000 / freq), 0, new MMTimerProc(TimerFunction), 0, 1);

        // wait 10 seconds
        while (DeltaMilliseconds(start, GetTimestamp()) < 10000)
        {
            Thread.Sleep(1);
        }

        // end timer
        timeKillEvent(timerId);

        Console.WriteLine("Requested frequency: {0}\nActual frequency: {1}\n", freq, count / 10);
    }

    Console.ReadLine();
}

static void TimerFunction(UInt32 timerid, UInt32 msg, IntPtr user, UInt32 dw1, UInt32 dw2)
{
    Interlocked.Increment(ref count);
}

static public long DeltaMilliseconds(long earlyTimestamp, long lateTimestamp)
{
    return (((lateTimestamp - earlyTimestamp) * 1000) / CPUFrequency);
}

static public long GetTimestamp()
{
    long result;
    QueryPerformanceCounter(out result);
    return result;
}

And here is the output I get:

Requested frequency: 5
Actual frequency: 5

Requested frequency: 10
Actual frequency: 10

Requested frequency: 15
Actual frequency: 15

Requested frequency: 20
Actual frequency: 19

Requested frequency: 30
Actual frequency: 30

Requested frequency: 50
Actual frequency: 50

Requested frequency: 75
Actual frequency: 76

Requested frequency: 100
Actual frequency: 100

Requested frequency: 200
Actual frequency: 200

Requested frequency: 500
Actual frequency: 500

Hope this helps.

五里雾 2024-07-17 12:38:14

这些类不适合实时使用,并且受 Windows 等操作系统的动态调度性质的影响。 如果您需要实时执行,您可能需要查看一些嵌入式硬件。 我不是 100% 确定,但我认为 .netcpu 可能是芯片上较小的 .NET 运行时的实时版本。

http://www.arm.com/markets/emerging_applications/armpp/8070。 html

当然 - 您需要评估这些间隔的准确性有多重要,因为附加到它们的代码将在非实时操作系统上执行。 当然,除非这是一个纯粹的学术问题(在这种情况下 - 是的,这很有趣!:P)。

These classes aren't intended for real-time use and are subject to the dynamic scheduling nature of an operating system like Windows. If you need real-time execution you'd probably want to look at some embedded hardware. I'm not 100% sure but I think .netcpu may be a real-time version of a smaller .NET runtime on a chip.

http://www.arm.com/markets/emerging_applications/armpp/8070.html

Of course - you need to evaluate how important accuracy of those intervals are given that the code attached to them is going to be executing on a non-realtime operating system. Unless of course this is a purely academic question (in which case - yes it is interesting! :P).

故事灯 2024-07-17 12:38:14

看起来您的实际计时器频率是 63.9 Hz 或其整数倍。

这意味着计时器分辨率约为 15 毫秒(或其整数倍,即 30 毫秒、45 毫秒等)。

定时器基于“tick”的整数倍,这是可以预料的(例如在 DOS 中,“tick”值为 55 毫秒/18 Hz)。

我不知道为什么你的滴答计数是 15.65 mec,而不是 15 毫秒。 作为一个实验,如果您在计时器处理程序中休眠几毫秒会怎样:我们可能会在滴答之间看到 15 毫秒,而在每次滴答时在计时器处理程序中休眠 0.65 毫秒吗?

It looks like your actual timer frequencies are 63.9 Hz or integer multiples thereof.

That would imply a timer resolution of about 15 msec (or integer multiples therefof, i.e. 30 msec, 45 msec, etc.).

This, timers based on integer multiples of a 'tick', is to be expected (in DOS for example the 'tick' value was 55 msec / 18 Hz).

I don't know why your tick count is 15.65 mec instead of an even 15 msec. As an experiment, what if you sleep for several msec within the timer handler: might we be seeing 15 msec between ticks, and 0.65 msec in your timer handler at each tick?

赴月观长安 2024-07-17 12:38:14

好吧,我实际上得到了高达 100 Hz 的不同数字,有一些很大的偏差,但在大多数情况下更接近请求的数字(使用最新的 .NET SP 运行 XP SP3)。

System.Timer.Timer 是使用 System.Threading.Timer 实现的,因此这解释了为什么您会看到相同的结果。 我认为计时器是使用某种调度算法等实现的(这是内部调用,也许查看 Rotor 2.0 可能会有所了解)。

我建议使用另一个线程(或其组合)调用 Sleep 和回调来实现一种计时器。 但不确定结果。

否则,您可以查看多媒体计时器 (P 调用)。

Well, I'm getting different number up to 100 Hz actually, with some big deviations, but in most cases closer to the requested number (running XP SP3 with most recent .NET SPs).

The System.Timer.Timer is implemented using System.Threading.Timer, so this explains why you see same results. I suppose that the timer is implemented using some kind of scheduling algorithm etc. (it's internal call, maybe looking at Rotor 2.0 might shed some light on it).

I would suggest to implement a kind of timer using another thread (or combination thereof) calling Sleep and a callback. Not sure about the outcome though.

Otherwise you might take a look at multimedia timers (PInvoke).

阳光的暖冬 2024-07-17 12:38:14

Windows(以及在其之上运行的.NET)是一个抢占式多任务操作系统。 任何给定的线程都可以随时被另一个线程停止,如果抢占线程行为不正常,您将无法在您想要或需要时重新获得控制权。

简而言之,这就是为什么不能保证您获得准确的时间,以及为什么 Windows 和 .NET 不适合某些类型的软件的平台。 如果由于您无法在需要时完全获得控制而导致生命处于危险之中,请选择其他平台。

Windows (and therefore .NET running on top of it) is a preemptively multitasking operating system. Any given thread can be stopped at any time by another thread, and if the preempting thread doesn't behave properly, you won't get control back when you want it or need it.

That, in a nutshell, is why you cannot be guaranteed to get exact timings, and why Windows and .NET are unsuitable platforms for certain types of software. If lives are in danger because you don't get control EXACTLY when you want it, choose a different platform.

坦然微笑 2024-07-17 12:38:14

如果您确实需要跳转到实时环境,我过去在需要确定性采样(来自自定义串行设备)时使用过 RTX,并且运气很好。

http://www.pharlap.com/rtx.htm

If you do need to make the jump to a real-time environment, I've used RTX in the past when deterministic sampling was needed (from a custom serial device) and had very good luck with it.

http://www.pharlap.com/rtx.htm

诗化ㄋ丶相逢 2024-07-17 12:38:14

定时器频率关闭的原因有很多。 以硬件为例,同一线程正忙于另一个处理,等等...

如果您想要更准确的时间,请使用 System.Diagnostics 命名空间中的 Stopwatch 类。

There's a lot of reasons that the timer frequencies off. Example with the hardware, the same thread is busy with another processing, and so on...

If you want more accurately time, use the Stopwatch class in the System.Diagnostics namespace.

情绪失控 2024-07-17 12:38:14

部分问题在于计时器需要考虑两次延迟。 这可能会有所不同,具体取决于计时器在操作系统中的实现方式。

  1. 您请求等待的时间
  2. #1 发生到进程在调度队列中轮到的时间之间的时间。

计时器对#1 有很好的控制,但对#2 几乎没有控制。 它可以向操作系统发出信号,表示它想再次运行,但操作系统可以在需要时将其唤醒。

Part of the problem is that Timers have two delays to account. This can vary depending on how the timer is implemented in the OS.

  1. The time that you request to wait for
  2. The time between when #1 occurs and the process gets its turn in the scheduling queue.

The timer has good control of #1 but almost no control over #2. It can signal to the OS that it would like to run again but the OS is free to wake it up when it feels like it.

暗地喜欢 2024-07-17 12:38:14

根据您的评论,您根本不应该使用计时器。 您应该使用带有秒表的循环来检查间隔和自旋锁,这样您就不会丢失量子。

Based on your comments, you shouldn't be using a timer at all. You should be using a loop with a stopwatch to check the interval and a spinlock so you don't lose the quantum.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文