.NET 6 新特性 PeriodicTimer
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