asp.net core托管服务中的“timer + Task.Run”与“while循环+ Task.Delay”

Dmi*_*nov 4 c# asynchronous timer task asp.net-core-hosted-services

我有一个要求,后台服务应该在Process每天凌晨 0:00 运行方法

因此,我的一位团队成员编写了以下代码:

public class MyBackgroundService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

    public MyBackgroundService(ILogger<MyBackgroundService> logger)
    {
        _logger = logger;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        TimeSpan interval = TimeSpan.FromHours(24);
        TimeSpan firstCall = DateTime.Today.AddDays(1).AddTicks(-1).Subtract(DateTime.Now);

        Action action = () =>
        {
            Task.Delay(firstCall).Wait();

            Process();

            _timer = new Timer(
                ob => Process(),
                null,
                TimeSpan.Zero,
                interval
            );
        };

        Task.Run(action);
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    private Task Process()
    {
        try
        {
            // perform some database operations
        }
        catch (Exception e)
        {
            _logger.LogError(e, e.Message);
        }
        return Task.CompletedTask;
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码按预期工作。但我不喜欢它同步等待直到Process第一次调用,因此线程被阻塞并且不执行任何有用的工作(如果我错了,请纠正我)。

我可以在其中创建一个异步操作并等待,如下所示:

public Task StartAsync(CancellationToken cancellationToken)
{
    // code omitted for brevity

    Action action = async () =>
    {
        await Task.Delay(firstCall);

        await Process();
        
        // code omitted for brevity
}
Run Code Online (Sandbox Code Playgroud)

但我不确定Task.Run这里使用是不是一件好事,因为Process方法应该执行一些 I/O 操作(查询数据库并插入一些数据),并且不建议在 ASP.NET 环境中使用Task.Run

我重构StartAsync如下:

public async Task StartAsync(CancellationToken cancellationToken)
{
    TimeSpan interval = TimeSpan.FromHours(24);
    TimeSpan firstDelay = DateTime.Today.AddDays(1).AddTicks(-1).Subtract(DateTime.Now);

    await Task.Delay(firstDelay);

    while (!cancellationToken.IsCancellationRequested)
    {
        await Process();

        await Task.Delay(interval, cancellationToken);
    }
}
Run Code Online (Sandbox Code Playgroud)

这允许我根本不使用计时器MyBackgroundService

我应该使用第一种方法“timer + Task.Run”还是第二种方法“while循环+ Task.Delay”?

The*_*ias 6

循环while方法更简单、更安全。使用该类Timer有两个隐藏的陷阱:

  1. 后续事件可能会以重叠方式调用附加的事件处理程序。
  2. 处理程序内引发的异常将被吞掉,并且此行为可能会在 .NET Framework 的未来版本中发生更改。(来自文档

不过,您当前的while循环实现可以通过多种方式进行改进:

  1. DateTime.Now在计算过程中多次读取TimeSpan可能会产生意外结果,因为每次DateTime返回的值可能不同。DateTime.Now最好将 存储DateTime.Now在变量中,并在计算中使用存储的值。
  2. 如果您还使用相同的标记作为 的参数,则检查循环cancellationToken.IsCancellationRequested中的条件可能会导致不一致的取消行为。完全跳过此检查更简单且一致。通过这种方式取消令牌总是会产生一个结果。whileTask.DelayOperationCanceledException
  3. 理想情况下,该持续时间Process不应影响下一个操作的调度。一种方法是Task.Delay在开始之前创建任务Process,并await在完成之后创建任务Process。或者您可以根据当前时间重新计算下一个延迟。这还有一个优点,即在系统时间发生变化的情况下,调度将自动调整。

这是我的建议:

public async Task StartAsync(CancellationToken cancellationToken)
{
    TimeSpan scheduledTime = TimeSpan.FromHours(0); // midnight
    TimeSpan minimumIntervalBetweenStarts = TimeSpan.FromHours(12);

    while (true)
    {
        var scheduledDelay = scheduledTime - DateTime.Now.TimeOfDay;

        while (scheduledDelay < TimeSpan.Zero)
            scheduledDelay += TimeSpan.FromDays(1);

        await Task.Delay(scheduledDelay, cancellationToken);

        var delayBetweenStarts =
            Task.Delay(minimumIntervalBetweenStarts, cancellationToken);

        await ProcessAsync();

        await delayBetweenStarts;
    }
}
Run Code Online (Sandbox Code Playgroud)

这样做的原因minimumIntervalBetweenStarts是为了防止系统时间发生非常剧烈的变化。