Intro
.NET 6 中引入了一個新的 Timer ——
System.Threading.PeriodicTimer,和之前的幾個 Timer 相比一個最大的區別就是,新的 PeriodicTimer 的事件處理可以比較方便地使用異步方式,消除了使用 callback 的機制,減少了使用的復雜度。
Sample
來看一個使用示例:
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true;
cts.Cancel();
};
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
try
{
while (await timer.WaitForNextTickAsync(cts.Token))
{
Console.WriteLine($"Timed event triggered({DateTime.Now:HH:mm:ss})");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation cancelled");
}
通常 PeriodicTimer 可以結合 CancellationToken 一起使用,和 CancellationToken 一起用的時候需要注意,如果 cancellationToken 被取消的時候會拋出一個
OperationCanceledException 需要考慮自己處理異常
除此之外如果 PeriodicTimer 被 Dispose,這個 timer 就相當于是失效的,并且無法重新恢復,來看下面這個示例:
var timer1 = new PeriodicTimer(TimeSpan.FromSeconds(2));
timer1.Dispose();
if (await timer1.WaitForNextTickAsync())
{
Console.WriteLine("Timer1 event triggered");
}
上面這樣的一段代碼,在 WaitForNextTickAsync 之前就已經調用了 Dispose(),此時 WaitForNextTickAsync 方法會始終返回 false ,所以 Console.WriteLine 的邏輯也不會被執行
我們之前會嘗試使用 Timer 來做一些后臺任務,可以改造成使用新的 PeriodicTimer 來實現,小示例如下:
public abstract class TimerScheduledService : BackgroundService
{
private readonly PeriodicTimer _timer;
private readonly TimeSpan _period;
protected readonly ILogger Logger;
protected TimerScheduledService(TimeSpan period, ILogger logger)
{
Logger = logger;
_period = period;
_timer = new PeriodicTimer(_period);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (await _timer.WaitForNextTickAsync(stoppingToken))
{
try
{
Logger.LogInformation("Begin execute service");
await ExecuteInternal(stoppingToken);
}
catch (Exception ex)
{
Logger.LogError(ex, "Execute exception");
}
finally
{
Logger.LogInformation("Execute finished");
}
}
}
catch (OperationCanceledException operationCancelledException)
{
Logger.LogWarning(operationCancelledException, "service stopped");
}
}
protected abstract Task ExecuteInternal(CancellationToken stoppingToken);
public override Task StopAsync(CancellationToken cancellationToken)
{
Logger.LogInformation("Service is stopping.");
_timer.Dispose();
return base.StopAsync(cancellationToken);
}
}
實現示例如下:
public class TimedHealthCheckService : TimerScheduledService
{
public TimedHealthCheckService(ILogger<TimedHealthCheckService> logger) : base(TimeSpan.FromSeconds(5), logger)
{
}
protected override Task ExecuteInternal(CancellationToken stoppingToken)
{
Logger.LogInformation("Executing...");
return Task.CompletedTask;
}
}
運行輸出如下:
More
新的 PeriodicTimer 相比之前的幾個 Timer 來說,有下面幾個特點
- 沒有 callback 來綁定事件
- 不會發生重入,只允許有一個消費者,不允許同一個 PeriodicTimer 在不同的地方同時 WaitForNextTickAsync,不需要自己做排他鎖來實現不能重入
- 異步化,之前的幾個 timer 的 callback 都是同步的,使用新的 timer 我們可以更好的使用異步方法,避免寫 Sync over Async 之類的代碼
- Dispose() 之后,該實例就無法再使用,WaitForNextTickAsync 始終返回 false
最后來做一個題目,把第一個示例改造一下,最終代碼如下:
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(30));
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
try
{
while (await timer.WaitForNextTickAsync(cts.Token))
{
await Task.Delay(5000);
Console.WriteLine($"Timed event triggered({DateTime.Now:HH:mm:ss})");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation cancelled");
}
猜一下輸出結果是什么,Timed event triggered 會輸出幾次
References
- https://www.ilkayilknur.com/a-new-modern-timer-api-in-dotnet-6-periodictimer
- https://docs.microsoft.com/en-us/dotnet/api/system.threading.periodictimer?view=net-6.0
- https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs
- https://github.com/dotnet/runtime/issues/31525
- https://github.com/WeihanLi/SamplesInPractice/blob/master/net6sample/PeriodicTimerSample/Program.cs
- https://github.com/OpenReservation/ReservationServer/blob/dev/OpenReservation.Helper/Services/CronScheduleServiceBase.cs#L91