问题描述
如何在极长的时间间隔(例如每天甚至更长)中在Kestrel进程中运行异步任务?该任务需要在Web服务器进程的内存空间中运行,以更新一些慢慢过期的全局变量。
错误答案:
- 尝试使用OS调度程序是一个糟糕的计划。
- 从控制器调用
await
是不可接受的。任务很慢。 - 对于
Task.Delay()
,延迟时间太长(大约16小时左右,Task.Delay
将会抛出)。 - HangFire等在这里毫无意义。这是一项内存中的工作,它并不关心数据库中的任何内容。此外,无论如何,如果没有用户上下文(登录用户点击某个控制器),我们就无法调用数据库。
-
System.Threading.Timer
。是可重入的。
奖金:
- 任务是幂等的。旧的运行完全无关紧要。
- 特定页面渲染是否错过更改无关紧要;下一个将很快得到它。
- 由于这是一台Kestrel服务器,因此我们并不担心停止后台任务。无论如何,当服务器进程关闭时,它将停止。
- 任务应在启动后立即运行一次。这应该使协调更加容易。
有些人错过了这个。该方法为async
。如果不是async
,这个问题将不会很困难。
解决方法
我将为此添加一个答案,因为这是在ASP.NET Core中完成此类操作的唯一逻辑方法:IHostedService
实现。
这是实现IHostedService
的不可重入计时器后台服务。
public sealed class MyTimedBackgroundService : IHostedService
{
private const int TimerInterval = 5000; // change this to 24*60*60 to fire off every 24 hours
private Timer _t;
public async Task StartAsync(CancellationToken cancellationToken)
{
// Requirement: "fire" timer method immediatly.
await OnTimerFiredAsync();
// set up a timer to be non-reentrant,fire in 5 seconds
_t = new Timer(async _ => await OnTimerFiredAsync(),null,TimerInterval,Timeout.Infinite);
}
public Task StopAsync(CancellationToken cancellationToken)
{
_t?.Dispose();
return Task.CompletedTask;
}
private async Task OnTimerFiredAsync()
{
try
{
// do your work here
Debug.WriteLine($"{TimerInterval / 1000} second tick. Simulating heavy I/O bound work");
await Task.Delay(2000);
}
finally
{
// set timer to fire off again
_t?.Change(TimerInterval,Timeout.Infinite);
}
}
}
因此,我知道我们在评论中对此进行了讨论,但是System.Threading.Timer
回调方法被视为事件处理程序。在这种情况下,perfectly acceptable使用async void
,因为在线程池线程上会引发一个转义该方法的异常,就像该方法是同步的一样。无论如何,您可能应该在其中丢一个catch
来记录任何异常。
您提出了在某个时间间隔边界不安全的计时器。我高低寻找这些信息,但找不到。我以24小时间隔,2天间隔,2周间隔使用计时器...我从来没有让它们失败过。我也有很多年在生产服务器的ASP.NET Core中运行。我们本来可以看到的。
好的,所以您仍然不信任System.Threading.Timer
...
比方说,不。。。没有使用计时器的怪癖。好,那很好...让我们再走一条路。让我们从IHostedService
转到BackgroundService
(这是IHostedService
的实现),然后简单地倒数。
这将减轻对计时器边界的任何担心,并且您不必担心async void
事件处理程序。这也是免费的不可重入帐户。
public sealed class MyTimedBackgroundService : BackgroundService
{
private const long TimerIntervalSeconds = 5; // change this to 24*60 to fire off every 24 hours
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Requirement: "fire" timer method immediatly.
await OnTimerFiredAsync(stoppingToken);
var countdown = TimerIntervalSeconds;
while (!stoppingToken.IsCancellationRequested)
{
if (countdown-- <= 0)
{
try
{
await OnTimerFiredAsync(stoppingToken);
}
catch(Exception ex)
{
// TODO: log exception
}
finally
{
countdown = TimerIntervalSeconds;
}
}
await Task.Delay(1000,stoppingToken);
}
}
private async Task OnTimerFiredAsync(CancellationToken stoppingToken)
{
// do your work here
Debug.WriteLine($"{TimerIntervalSeconds} second tick. Simulating heavy I/O bound work");
await Task.Delay(2000);
}
}
额外的副作用是,您可以使用long
作为间隔时间,从而使事件有25天以上的时间触发,而Timer
的上限是25天。
您可以这样注入其中的任何一个:
services.AddHostedService<MyTimedBackgroundService>();