我想到了创建一个可以等待的计时器,而不是引发事件。我还没有想到任何实际的应用程序,并且可能不是非常有用的东西,但是我想看看它是否至少可以作为练习来使用。这是可以使用的方式:
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描述了如何使各种事物等待,例如TimeSpan
,int
,DateTimeOffset
和{{1} }。看来我必须编写一个返回Process
的扩展方法,但是我不确定该怎么做。有人知道吗?
TaskAwaiter
更新:我使用执行accepted answer的实际输出更新了示例代码和预期的输出。
答案 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:完成。我根据西奥多的隐式挑战实施了一些“控制器”构想;)
此版本尚未经过测试,因此仅用于传达想法。此外,对于任何生产版本,都需要更好的并发服务。