如何在 Kestrel 进程中每天 运行 一个异步任务?

How to run an async task daily in a Kestrel process?

如何 运行 Kestrel 进程中的异步任务具有很长的时间间隔(例如每天或什至更长)?该任务需要在 Web 服务器进程的内存 space 中 运行 更新一些慢慢过时的全局变量。

错误答案:

奖金:

有些人忽略了这一点。方法是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>();