如何制作一个在主线程中触发事件的C#定时器?

16

长话短说,我需要在 .Net 中使用一个精确的计时器 - 毫秒级别的精度 - 这意味着,如果我让它在10毫秒后触发一个事件,它必须按照+-1毫秒的精度执行。内置的 .Net 计时器类似乎只有+-16毫秒的精度,这对我的应用程序是无法接受的。

我找到了这篇文章http://www.codeproject.com/Articles/98346/Microsecond-and-Millisecond-NET-Timer,其中提供了一个代码,可以提供我所需要的计时器(甚至具有微秒级别的精度)。

但是,问题是等价于 OnTimer 的代码似乎在另一个线程中执行。因此,如果我添加一些代码,例如:

label1.Text = "Hello World";

我会得到一个异常,因此我必须像这样实际编写它:

Invoke( new MethodInvoker(() =>{label1.Text = "Hello World";}));

这是我所了解的,因为OnTimer事件是从定时器线程触发的 - 在那里时间被传递,直到足够的时间已经过去以超过Interval,然后触发下一个OnTimer事件。 .Net计时器没有这样的问题 - 在.Net计时器的OnTimer中,我可以自由修改控件的成员。
问题:我应该改变什么,使得我的计时器在主线程运行它的OnTimer事件?添加“Invoke”是唯一的选择吗?

2
Forms计时器之所以不会受到这种影响,仅仅是因为它作为一种便利,代表您进行了调用。如果您使用其他计时器,您需要像您在问题中描述的那样进行调用。 - spender
1
值得记住的是,Windows 不是一个实时操作系统。试图以1毫秒的精度实现任何操作可能会很棘手。 - spender
为什么需要如此高的吞吐量?你永远无法以如此快的速度更新UI。无论如何,Windows窗体计时器是一种很好的选择。它是一个包装OS计时器的UI线程触发器,并且是您能找到的最快的该类工具。 - Eli Arbel
2
我想知道,如果您的显示器延迟几毫秒,为什么需要精确到1毫秒来更改标签。 - dtb
是的,我有点明白,在编写代码的过程中,即使在120Hz的显示器上,我们仍然只有8ms的GUI精度,因为我们每8ms才能更改帧。因此,在UI部分争取精度并没有太大意义。但是这不仅仅涉及UI,所以我仍然需要一个精确的计时器。 - Istrebitel
如果 .Net Timer 确实只是为我“调用”,那么问题就解决了,必须调用。我也会尝试下面得到最多票数的解决方案。 - Istrebitel
3个回答

16

虽然有几种方法可以实现,但我通常更倾向于在计时器创建时捕获SynchronizationContext.Current的值。当在UI线程中时,该值将包含当前同步上下文,可用于在UI上下文中执行消息循环中的方法。这适用于winforms、WPF、silverlight等所有这些范例都设置了同步上下文。

只需要在计时器创建时抓取该值,前提是它在UI线程中创建。如果您想要使用可选的构造函数/属性来设置该值,以便即使计时器不在UI线程中也可以使用它,那么可以这样做,尽管大多数情况下不需要。

之后,只需使用该上下文的Send方法来触发事件:

public class Timer
{
    private SynchronizationContext syncContext;
    public Timer()
    {
        syncContext = SynchronizationContext.Current;
    }

    public event EventHandler Tick;

    private void OnTick()
    {
        syncContext.Send(state =>
        {
            if (Tick != null)
                Tick(this, EventArgs.Empty);
        }, null);
    }

    //TODO other stuff to actually fire the tick event
}

我认为使用 Post 更好。使用 Send 会浪费很多不必要的同步时间。不过,Windows Forms 计时器会产生更好的结果,因为它根本不需要同步 - 它直接向窗口发送消息,而不是启动线程池线程。 - Eli Arbel
@EliArbel 使用Send还是Post取决于您是否希望计时器在处理程序全部完成之前等待,或者不等待。至于使用其他计时器; OP明确表示在此上下文中这不是他的选择,并且他需要使用自己的计时器,然后调度到UI线程。这正是它所做的。 - Servy
这个会在主线程中以10毫秒的间隔和+/- 1毫秒的准确度执行Tick处理程序,还是只有syncContext.Send调用符合要求? - dtb
我的观点是,使用它肯定比使用Windows Forms计时器慢,因为瓶颈在于窗口的消息泵,而不是计时器。 - Eli Arbel
2
@dtb 显然,直到消息泵到达该方法时,该方法才会被执行。如果队列为空,则这应该与任何其他调用 UI 线程的方法一样快,尽管可能不够快。我不认为还有其他方法可以更快。 - Servy
@EliArbel 这有可能比其他计时器更快地将项目添加到Windows消息泵中,如果它基于具有更高精度的tick事件触发。无论队列是否能够快速处理它都是两个计时器都会遇到的问题,这可能对OP来说是一个问题,也可能不是。无论如何,这回答了提出的问题。如果他说它不起作用,那么很可能什么都不会发生。如果它确实起作用,那就对他有好处。 - Servy

4

无论如何,都要将UI元素访问调度到主线程。如果在计时器回调中更新UI元素确实是您唯一的意图,则忘记有关计时器精度要求的一切。用户看不到16毫秒和50毫秒之间的差异。

否则,在计时器回调中执行时间关键型工作,并将其余的UI工作调度到主线程中:

void OnTimer()
{
  // time critical stuff here

  Invoke( new MethodInvoker(() =>{label1.Text = "Hello World";}));
}

是的,我有点明白,在编写代码的过程中,即使在120Hz的显示器上,我们仍然只有8ms的GUI精度,因为我们每8ms才能更改帧。因此,在UI部分争取精度并没有太大意义。但是这不仅仅涉及UI,所以我仍然需要一个精确的计时器。 - Istrebitel
@Istrebitel 这就是我在帖子中提到的原因 :-) - Oliver Weichhold

2
在 WPF 中,您可以使用 dispatcher 类来在 UI 线程中分派消息:
Dispatcher.CurrentDispatcher.BeginInvoke(
      new Action(()=> label1.Text = "Hello World"));

在WinForms中,您需要调用invoke方法:
this.Invoke(()=> label1.Text = "Hello World");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接