如何在 Kestrel 进程中每天 运行 一个异步任务?
How to run an async task daily in a Kestrel process?
如何 运行 Kestrel 进程中的异步任务具有很长的时间间隔(例如每天或什至更长)?该任务需要在 Web 服务器进程的内存 space 中 运行 更新一些慢慢过时的全局变量。
错误答案:
- 尝试使用 OS 调度程序是一个糟糕的计划。
- 从控制器调用
await
是不可接受的。任务很慢。
Task.Delay()
延迟太长(大约 16 小时左右,Task.Delay
会抛出)。
- HangFire 等在这里毫无意义。这是一个内存作业,不关心数据库中的任何内容。此外,无论如何,我们不能在没有用户上下文(从登录用户点击某个控制器)的情况下调用数据库。
System.Threading.Timer
。它是可重入的。
奖金:
- 任务是幂等的。旧的 运行 完全无关紧要。
- 特定页面呈现是否错过更改并不重要;下一个很快就会得到它。
- 由于这是一个 Kestrel 服务器,我们并不真正担心停止后台任务。不管怎样,当服务器进程宕机时它就会停止。
- 任务应该 运行 在启动时立即执行一次。这应该使协调更容易。
有些人忽略了这一点。方法是async
。如果不是 async
问题就不难了。
我将对此添加一个答案,因为这是在 ASP.NET 核心中完成此类事情的唯一合乎逻辑的方法:IHostedService
实现。
这是实现 IHostedService
的 non-reentrant 定时器后台服务。
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
回调方法被认为是 事件处理程序 。在这种情况下使用 async void
是 ,因为转义该方法的异常将在线程池线程上引发,就像该方法是同步的一样。你可能应该在那里抛出一个 catch
来记录任何异常。
您提出计时器在某些间隔边界不安全。我四处寻找该信息,但找不到。我以 24 小时间隔、2 天间隔、2 周间隔使用计时器……我从未让它们失败。多年来,我在生产服务器的 ASP.NET 核心中也有很多 运行。我们现在就可以看到它发生了。
好的,所以你还是不相信System.Threading.Timer
...
让我们这么说吧,不...您根本没有办法使用计时器。好吧,没关系……让我们走另一条路。让我们从 IHostedService
移动到 BackgroundService
(这是 IHostedService
的实现)并简单地倒数。
这将消除对计时器边界的任何担忧,您不必担心 async void
事件处理程序。这也是免费的non-reentrant
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);
}
}
一个好处 side-effect 是你可以使用 long
作为你的间隔,允许你有超过 25 天的时间来触发事件,而不是 Timer
的上限是 25 天.
您可以这样注入其中之一:
services.AddHostedService<MyTimedBackgroundService>();
如何 运行 Kestrel 进程中的异步任务具有很长的时间间隔(例如每天或什至更长)?该任务需要在 Web 服务器进程的内存 space 中 运行 更新一些慢慢过时的全局变量。
错误答案:
- 尝试使用 OS 调度程序是一个糟糕的计划。
- 从控制器调用
await
是不可接受的。任务很慢。 Task.Delay()
延迟太长(大约 16 小时左右,Task.Delay
会抛出)。- HangFire 等在这里毫无意义。这是一个内存作业,不关心数据库中的任何内容。此外,无论如何,我们不能在没有用户上下文(从登录用户点击某个控制器)的情况下调用数据库。
System.Threading.Timer
。它是可重入的。
奖金:
- 任务是幂等的。旧的 运行 完全无关紧要。
- 特定页面呈现是否错过更改并不重要;下一个很快就会得到它。
- 由于这是一个 Kestrel 服务器,我们并不真正担心停止后台任务。不管怎样,当服务器进程宕机时它就会停止。
- 任务应该 运行 在启动时立即执行一次。这应该使协调更容易。
有些人忽略了这一点。方法是async
。如果不是 async
问题就不难了。
我将对此添加一个答案,因为这是在 ASP.NET 核心中完成此类事情的唯一合乎逻辑的方法:IHostedService
实现。
这是实现 IHostedService
的 non-reentrant 定时器后台服务。
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
回调方法被认为是 事件处理程序 。在这种情况下使用 async void
是 catch
来记录任何异常。
您提出计时器在某些间隔边界不安全。我四处寻找该信息,但找不到。我以 24 小时间隔、2 天间隔、2 周间隔使用计时器……我从未让它们失败。多年来,我在生产服务器的 ASP.NET 核心中也有很多 运行。我们现在就可以看到它发生了。
好的,所以你还是不相信System.Threading.Timer
...
让我们这么说吧,不...您根本没有办法使用计时器。好吧,没关系……让我们走另一条路。让我们从 IHostedService
移动到 BackgroundService
(这是 IHostedService
的实现)并简单地倒数。
这将消除对计时器边界的任何担忧,您不必担心 async void
事件处理程序。这也是免费的non-reentrant
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);
}
}
一个好处 side-effect 是你可以使用 long
作为你的间隔,允许你有超过 25 天的时间来触发事件,而不是 Timer
的上限是 25 天.
您可以这样注入其中之一:
services.AddHostedService<MyTimedBackgroundService>();