提高WPF DispatcherTimer精度

目录

介绍

2个Ticks之间WPF需要的最短时间

增加用于DispatcherTimer的DispatcherPriority

为间隔选择一个现实的持续时间

使tick每100毫秒可靠地增加


介绍

我写了一个开源的WPF游戏MasterGrab,它已经成功运行了6年。在这个游戏中,人类玩家与多个机器人对战。最近,我想扩展游戏,让机器人可以相互玩耍。用户可以选择机器人每秒应该移动多少次,每次移动都显示在屏幕上。

我认为使用DispatcherTimer会很容易实现,它每x秒在WPF线程上引发一个Tick事件。此持续时间由DispatcherTimer.Interval控制。当我将它设置为100毫秒时,我注意到我在不规则的时间每秒只看到大约4次移动。因此,我开始研究其DispatcherTimer行为方式以及如何改进。

2Ticks之间WPF需要的最短时间

那么,第一个问题是:DispatcherTimer能跑多快?为了在我的PC上测量它,我编写了一个WPF应用程序,它仅仅运行DispatcherTimerXAML窗口仅包含一个名称为MainTextBlockTextBlock

using System;
using System.Text;
using System.Windows;
using System.Windows.Threading;

namespace WpfTimer {
  public partial class MainWindow: Window {
    DispatcherTimer timer;

    public MainWindow() {
      InitializeComponent();

      timer = new();
      timer.Interval = TimeSpan.FromMilliseconds(0);
      timer.Tick += Timer_Tick;
      timer.Start();
    }

    const int timesCount = 20;
    DateTime[] times = new DateTime[timesCount];
    int timesIndex;

    private void Timer_Tick(object? sender, EventArgs e) {
      times[timesIndex] = DateTime.Now;
      if (++timesIndex>=timesCount) {
        timer.Stop();
        var sb = new StringBuilder();
        var startTime = times[0];
        for (int i = 1; i < timesCount; i++) {
          var time = times[i];
          sb.AppendLine($"{(time - startTime):ss\\.fff} | 
                       {(int)(time - times[i-1]).TotalMilliseconds, 3:##0}");
        }
        MainTextBox.Text = sb.ToString();
      }
    }
  }
}

输出是:

00.021 |  21
00.021 |   0
00.021 |   0
00.021 |   0
...

对于每个tick,有一行。第一列显示这个tick发生后有多少seconds.milliseconds。第二列显示了此tick和上一个tick之间经过了多长时间。

由于我将Interval设置为0,因此在tick之间没有时间损失,除了第一个tick和第二个tick之间。显然,将Interval设置为0没有意义,我本以为会出现异常。

这是结果Interval = 1 millisecond

00.192 | 192
00.215 |  22
00.219 |   3
00.235 |  15
00.471 | 236
00.600 | 128
00.743 | 142
00.764 |  21
00.935 | 170
01.239 | 303
01.326 |  87
01.628 | 302
01.894 | 266
02.210 | 316
02.375 | 164
02.435 |  60
02.527 |  92
02.658 | 131
02.685 |  26

我没想到Tick会每毫秒提高一次,但我惊讶地发现2tick之间可能会经过300多毫秒,考虑到我的应用程序除了运行计时器之外什么都不做。

增加用于DispatcherTimerDispatcherPriority

然后我注意到DispatcherTimer构造函数可以带一个Dispatcher<wbr />Priority参数,该参数似乎设置为Background,这意味着计时器只会运行一次所有其他非空闲操作都完成

Name      Priority Description
Invalid         -1 This is an invalid priority.
Inactive         0 Operations are not processed.
SystemIdle       1 Operations are processed when the system is idle.
ApplicationIdle  2 Operations are processed when the application is idle.
ContextIdle      3 Operations are processed after background operations have completed.
Background       4 Operations are processed after all other non-idle operations are completed.
Input            5 Operations are processed at the same priority as input.
Loaded           6 Operations are processed when layout and render has finished but just 
                   before items at input priority are serviced. Specifically this is used 
                   when raising the Loaded event.
Render           7 Operations processed at the same priority as rendering.
DataBind         8 Operations are processed at the same priority as data binding.
Normal           9 Operations are processed at normal priority. This is the typical 
                   application priority.
Send            1o Operations are processed before other asynchronous operations. This is 
                   the highest priority.

我在我的游戏应用程序中尝试了我可以使用的最高优先级。显然,必须在显示下一步之前完成渲染。因此,让我们看看Tick使用Dispatcher<wbr />Priority.Input时提升的速度有多快:

00.014 |  14
00.024 |   9
00.091 |  67
00.202 | 111
00.221 |  19
00.226 |   4
00.242 |  16
00.272 |  30
00.307 |  34
00.369 |  61
00.460 |  91
00.493 |  33
00.524 |  30
00.555 |  31
00.586 |  30
00.712 | 125
00.745 |  33
00.761 |  15
00.788 |  27

为间隔选择一个现实的持续时间

我想说它现在可以快近3倍的速度运行。显然,Interval=1millisecond没有真正的意义。那么Interval=100 msec怎么样?

00.193 | 193
00.292 |  98
00.417 | 124
00.592 | 174
00.718 | 126
00.876 | 157
01.001 | 125
01.142 | 141
01.263 | 120
01.392 | 129
01.559 | 166
01.677 | 117
01.872 | 195
02.010 | 137
02.143 | 133
02.256 | 113
02.358 | 101
02.472 | 114
02.589 | 116

糟糕,现在2tick之间所需的时间几乎总是显着超过100毫秒,而我每秒只得到大约7个而不是10tick:-( 问题似乎是在x毫秒的随机延迟后,计时器再次等待100毫秒,而不是100-x毫秒。

使tick100毫秒可靠地增加

所以基本上,我们必须告诉在每个tick事件期间,Interval应该有多少毫秒长,直到下一个tick

const int constantInterval = 100;//milliseconds

private void Timer_Tick(object? sender, EventArgs e) {
  var now = DateTime.Now;
  var nowMilliseconds = (int)now.TimeOfDay.TotalMilliseconds;
  var timerInterval = constantInterval - 
   nowMilliseconds%constantInterval + 5;//5: sometimes the tick comes few millisecs early
  timer.Interval = TimeSpan.FromMilliseconds(timerInterval);

代码是这样工作的。它试图每0.1秒准确地进行一次tick。因此,如果第一个tick发生在142毫秒之后,则Interval设置为58毫秒而不是100

00.093 |  93
00.216 | 122
00.311 |  95
00.408 |  96
00.515 | 106
00.611 |  96
00.730 | 119
00.859 | 128
00.929 |  70
00.995 |  65
01.147 | 152
01.209 |  62
01.314 | 104
01.402 |  87
01.496 |  94
01.621 | 125
01.731 | 109
01.794 |  63
01.936 | 141

最后!现在我每秒有10tick。当然,也不完全是每100毫秒,因为有时,WPF线程仍然需要太多时间来进行其他活动。但是当这种情况发生时,计时器至少会尝试更快地提高下一个tick

https://www.codeproject.com/Articles/5323994/Improving-the-WPF-DispatcherTimer-Precision

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值