创建一个等待的System.Timers.Timer

时间:2019-07-02 01:10:20

标签: c# .net timer async-await

我想到了创建一个可以等待的计时器,而不是引发事件。我还没有想到任何实际的应用程序,并且可能不是非常有用的东西,但是我想看看它是否至少可以作为练习来使用。这是可以使用的方式:

var timer = new System.Timers.Timer();
timer.Interval = 100;
timer.Enabled = true;
for (int i = 0; i < 10; i++)
{
    var signalTime = await timer;
    Console.WriteLine($"Awaited {i}, SignalTime: {signalTime:HH:mm:ss.fff}");
}

等待计时器10次,预期输出为:

  

等待0,SignalTime:06:08:51.674
  等待1,SignalTime:06:08:51.783
  等待2,SignalTime:06:08:51.891
  等待3,SignalTime:06:08:52.002
  等待4,SignalTime:06:08:52.110
  等待5,SignalTime:06:08:52.218
  等待6,SignalTime:06:08:52.332
  等待7,SignalTime:06:08:52.438
  等待8,SignalTime:06:08:52.546
  等待9,SignalTime:06:08:52.660

在这种情况下,简单的await Task.Delay(100)会做同样的事情,但是计时器提供了灵活性,可以控制与程序另一部分之间的间隔(可能要注意thread safety issues)。

关于实现,我发现an article描述了如何使各种事物等待,例如TimeSpanintDateTimeOffset和{{1} }。看来我必须编写一个返回Process的扩展方法,但是我不确定该怎么做。有人知道吗?

TaskAwaiter

更新:我使用执行accepted answer的实际输出更新了示例代码和预期的输出。

2 个答案:

答案 0 :(得分:3)

  

似乎我必须编写一个扩展方法来返回TaskAwaiter,但是我不确定该怎么做。

返回等待者的最简单方法是获取Task,然后在其上调用GetAwaiter。您还可以创建自定义的等待者,但这涉及更多。

因此问题变成“事件引发后如何完成任务?”答案是use TaskCompletionSource<T>

public static class TimerExtensions
{
    public static Task<DateTime> NextEventAsync(this Timer timer)
    {
        var tcs = new TaskCompletionSource<DateTime>();
        ElapsedEventHandler handler = null;
        handler = (_, e) =>
        {
            timer.Elapsed -= handler;
            tcs.TrySetResult(e.SignalTime);
        };
        timer.Elapsed += handler;
        return tcs.Task;
    }

    public static TaskAwaiter<DateTime> GetAwaiter(this Timer timer)
    {
        return timer.NextEventAsync().GetAwaiter();
    }
}

因此,这将使您的示例代码按预期工作。但是,有一个重要警告:每个await都将调用GetAwaiter,这将订阅下一个Elapsed事件。并且Elapsed事件处理程序将在await完成之前被删除。因此,从事件触发到下一次等待计时器开始,没有 处理程序,而且您使用的代码很容易错过某些事件。

如果这是不可接受的,则应使用IObservable<T>(它是围绕“订阅-然后-接收-事件”模型设计的),或使用类似Channels的东西来缓冲事件并通过异步流使用它们。

答案 1 :(得分:0)

Task.Delay仍然是供您使用的正确构建块。

代码

//Uses absolute time from the first time called to synchronise the caller to begin on the next pulse
//If the client takes 2.5xinterval to perform work, the next pulse will be on the 3rd interval
class AbsolutePollIntervals
{
    TimeSpan interval = TimeSpan.Zero;
    public AbsolutePollIntervals(TimeSpan Interval)
    {
        this.interval = Interval;
    }

    //Call this if you want the timer to start before you await the first time
    public void Headstart()
    {
        started = DateTime.UtcNow;
    }

    public void StopCurrentEarly()
    {
        cts.Cancel(); //Interrupts the Task.Delay in DelayNext early
    }

    public void RaiseExceptionOnCurrent(Exception ex)
    {
        nextException = ex; //This causes the DelayNext function to throw this exception to caller
        cts.Cancel();
    }

    public void RepeatCurrent()
    {
        delayAgain = true; //This cuases DelayNext to loop again. Use this with SetNextInterval, if you wanted to extend the delay
        cts.Cancel();
    }

    public void SetNextInterval(TimeSpan interval)
    {
        started = DateTime.MinValue; //No headstart
        this.interval = interval;
    }

    Exception nextException = null;
    DateTime started = DateTime.MinValue;
    CancellationTokenSource cts = null;
    bool delayAgain = false;

    public async Task DelayNext()
    {
        while (true)
        {
            if (started == DateTime.MinValue) started = DateTime.UtcNow;
            var reference = DateTime.UtcNow;
            var diff = reference.Subtract(started);

            var remainder = diff.TotalMilliseconds % interval.TotalMilliseconds;
            var nextWait = interval.TotalMilliseconds - remainder;

            cts = new CancellationTokenSource();
            
            await Task.Delay((int)nextWait, cts.Token);
            cts.Dispose();

            if (nextException != null)
            {
                var ex = nextException; //So we can null this field before throwing
                nextException = null;
                throw ex;
            }

            if (delayAgain == false)
                break;
            else
                delayAgain = false; //reset latch, and let it continue around another round
        }
    }
}

对消费者的使用

var pacer = new AbsolutePollIntervals(TimeSpan.FromSeconds(1));
for (int i = 0; i < 10; i++)
{
    await pacer.DelayNext();
    Console.WriteLine($"Awaited {i}, SignalTime: {DateTime.UtcNow:HH:mm:ss.fff}");
}
return;

在控制器上的用法:

//Interrupt the consumer early with no exception
pacer.StopCurrentEarly();

//Interrupt the consumer early with an exception
pacer.RaiseExceptionOnCurrent(new Exception("VPN Disconnected"));

//Extend the time of the consumer by particular amount
pacer.SetNextInterval(TimeSpan.FromSeconds(20));
pacer.RepeatCurrent();

结果 [在编辑之前,未测试当前版本]

等待0,SignalTime:03:56:04.777
等待1,SignalTime:03:56:05.712
等待2,SignalTime:03:56:06.717
等待3,SignalTime:03:56:07.709
等待4,SignalTime:03:56:08.710
等待5,SignalTime:03:56:09.710
等待6,SignalTime:03:56:10.710
等待7,SignalTime:03:56:11.709
等待8,SignalTime:03:56:11.709
等待9,SignalTime:03:56:12.709

如您在上面看到的,它们都降落在710ms标记附近,表明这是一个绝对间隔(与相对于调用DelayNext的持续时间无关)


“控制器”可能会与“笨拙的人”持有AbsolutePollIntervals的共享引用。通过扩展AbsolutePollIntervals,控制器可以更改间隔和开始时间。还可以创建一个QueuedPollIntervals实现,其中控制器排队由重做者在DelayNext()

上出队的不同时间间隔。

更新2020-09-20:完成。我根据西奥多的隐式挑战实施了一些“控制器”构想;)

此版本尚未经过测试,因此仅用于传达想法。此外,对于任何生产版本,都需要更好的并发服务。