简体   繁体   English

Timer 是否应该在触发新的回调之前等待回调完成?

[英]Should Timer be waiting for callback to finish before firing a new one?

I am using the System.Threading.Timer class in one of my projects and I've noticed that the callback methods are called before the previous ones get to finish which is different than what I was expecting.我在我的一个项目中使用System.Threading.Timer类,我注意到回调方法在之前的方法完成之前被调用,这与我的预期不同。

For example the following code例如下面的代码

var delta = new TimeSpan(0,0,1);
var timer = new Timer(TestClass.MethodAsync, null, TimeSpan.Zero, delta);

static class TestClass
{
    static int i = 0;
    public static async void MethodAsync(object _)
    {
        i++;
        Console.WriteLine("method " + i + "started");

        await Task.Delay(10000);
        
        Console.WriteLine("method " + i + "finished");
    }
}

has this output有这个输出

method 1started
method 2started
method 3started
method 4started
method 5started
method 6started
method 7started
method 8started
method 9started
method 10started
method 11started
method 11finished
method 12started
method 12finished

Which of course is not thread safe.这当然不是线程安全的。 What I was expecting is that a new call would be made to the callback method after the previous call has succeeded and additionally after the delta period is elapsed.我所期待的是,在前一次调用成功之后以及在delta周期过去之后,将对回调方法进行新的调用。

What I am looking for is where in the docs from Microsoft is this behavior documented and maybe if there is a built in way to make it wait for the callback calls to finish before starting new ones我正在寻找的是 Microsoft 的文档中记录了这种行为的位置,也许是否有一种内置方法可以让它在开始新的调用之前等待回调调用完成

What I am looking for is where in the docs from Microsoft is this behavior documented...我正在寻找的是微软的文档中记录了这种行为的地方......

System.Threading.Timer System.Threading.Timer

If processing of the Elapsed event lasts longer than Interval, the event might be raised again on another ThreadPool thread.如果 Elapsed 事件的处理持续时间超过 Interval,则该事件可能会在另一个 ThreadPool 线程上再次引发。 In this situation, the event handler should be reentrant.在这种情况下,事件处理程序应该是可重入的。

System.Timers.Timer System.Timers.Timer

The callback method executed by the timer should be reentrant, because it is called on ThreadPool threads.定时器执行的回调方法应该是可重入的,因为它是在 ThreadPool 线程上调用的。

For System.Windows.Forms.Timer this post asserts that the event does wait.对于System.Windows.Forms.Timer ,这篇文章断言事件确实在等待。 The documentation doesn't seem very specific, but in the Microsoft Timer.Tick Event official example the code shows turning the timer off and on in the handler .文档似乎不是很具体,但在 Microsoft Timer.Tick Event官方示例中,代码显示了在 handler 中关闭和打开计时器。 So it seems that, regardless, steps are taken to prevent ticks and avoid reentrancy.因此,无论如何,似乎都采取了措施来防止滴答声并避免重入。

...and if there is a built in way to make it wait for the callback calls to finish before starting new ones. ...并且如果有一种内置方法可以让它在开始新的调用之前等待回调调用完成。

According to the first Microsoft link (you might consider this a workaround, but it's straight from the horse's mouth):根据第一个 Microsoft 链接(您可能认为这是一种解决方法,但它直接来自马的嘴):

One way to resolve this race condition is to set a flag that tells the event handler for the Elapsed event to ignore subsequent events.解决这种竞争条件的一种方法是设置一个标志,告诉 Elapsed 事件的事件处理程序忽略后续事件。

The way I personally go about achieving this objective this is to call Wait(0) on the synchronization object of choice as a robust way to ignore reentrancy without having timer events piling up in a queue:我个人实现这个目标的方法是在选择的同步对象上调用Wait(0)作为一种健壮的方法来忽略重入,而不会在队列中堆积计时器事件:

static SemaphoreSlim _sslim = new SemaphoreSlim(1, 1);
public static async void MethodAsync(object _)
{
    if (_sslim.Wait(0))
    {
        try
        {
            i++;
            Console.WriteLine($"method {i} started @ {DateTime.Now}");

            await Task.Delay(10000);

            Console.WriteLine($"method {i} finished @ {DateTime.Now}");
        }
        catch (Exception ex)
        {
            Debug.Assert(false, ex.Message);
        }
        finally
        {
            _sslim.Release();
        }
    }
}

In which case your MethodAsync generates this output:在这种情况下,您的MethodAsync生成此输出:

在此处输入图像描述

The problem of overlapping event handlers is inherent with the classic multithreaded .NET timers (the System.Threading.Timer and the System.Timers.Timer ).重叠事件处理程序的问题是经典多线程 .NET 计时器( System.Threading.TimerSystem.Timers.Timer )所固有的。 Attempting to solve this problem while remaining on the event-publisher-subscriber model is difficult, tricky, and error prone.在保留事件发布者订阅者模型的同时尝试解决此问题是困难、棘手且容易出错的。 The .NET 6 introduced a new timer, the PeriodicTimer , that attempts to solve this problem once and for all. .NET 6 引入了一个新的计时器PeriodicTimer ,它试图一劳永逸地解决这个问题。 Instead of handling an event, you start an asynchronous loop and await the PeriodicTimer.WaitForNextTickAsync method on each iteration.您无需处理事件,而是启动异步循环并在每次迭代时await PeriodicTimer.WaitForNextTickAsync方法。 Example:例子:

class TestClass : IDisposable
{
    private int i = 0;
    private PeriodicTimer _timer;

    public async Task StartAsynchronousLoop()
    {
        if (_timer != null) throw new InvalidOperationException();
        _timer = new(TimeSpan.FromSeconds(1));
        while (await _timer.WaitForNextTickAsync())
        {
            i++;
            Console.WriteLine($"Iteration {i} started");
            await Task.Delay(10000); // Simulate an I/O-bound operation
            Console.WriteLine($"Iteration {i} finished");
        }
    }

    public void Dispose() => _timer?.Dispose();
}

This way there is no possibility for overlapping executions, provided that you will start only one asynchronous loop.这样就不可能发生重叠执行,前提是您只启动一个异步循环。

The await _timer.WaitForNextTickAsync() returns false when the timer is disposed. await _timer.WaitForNextTickAsync()在定时器被释放时返回false You can also stop the loop be passing a CancellationToken as argument.您还可以停止循环,将CancellationToken作为参数传递。 When the token is canceled, the WaitForNextTickAsync will complete with an OperationCanceledException .当令牌被取消时, WaitForNextTickAsync将以OperationCanceledException完成。

In case the periodic action is not asynchronous, you can offload it to the ThreadPool , by wrapping it in Task.Run :如果周期性操作不是异步的,您可以将其卸载到ThreadPool ,方法是将其包装在Task.Run中:

await Task.Run(() => Thread.Sleep(10000)); // Simulate a blocking operation

If you are targeting a .NET platform older than .NET 6, you can find alternatives to the PeriodicTimer here .如果您的目标是早于 .NET 6 的 .NET 平台,您可以在此处找到PeriodicTimer的替代品。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM