c# - asp.net 核心托管服务中的 "timer + Task.Run"与 "while loop + Task.Delay"

标签 c# asynchronous timer task asp.net-core-hosted-services

我要求后台服务每天凌晨 0:00 运行 Process 方法

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

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;
    }
}

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

我可以像这样让一个 Action 异步并在其中等待:

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

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

        await Process();
        
        // code omitted for brevity
}

但我不确定在这里使用 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);
    }
}

这让我可以完全不在 MyBackgroundService 中使用计时器。

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

最佳答案

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

  1. 后续事件可能会以重叠方式调用附加的事件处理程序。
  2. 处理程序内部抛出的异常被吞没,此行为在 .NET Framework 的 future 版本中可能会发生变化。(来自 docs)

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

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

这是我的建议:

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;
    }
}

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

关于c# - asp.net 核心托管服务中的 "timer + Task.Run"与 "while loop + Task.Delay",我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64517214/

相关文章:

c# - 直接添加引用和通过Nuget添加包有什么区别

c# - 如何更新 C# Windows 控制台应用程序中的当前行?

c# - 有没有正确的方法来取消异步任务?

javascript - Angular 4 - 循环遍历 Observables 并在新数组中提取值

java - 如何根据已过去的时间更改 Java 中哈希表内的值?

c# - 将下拉列表传递到方法中

iphone - 当 dispatch_async 任务完成时如何通知我?

javascript - 调用从 00m :00s after certain time 开始的定时器

c - 我如何创建一个与 C 游戏同时工作的计时器?

c# - 在 Windows 通用应用程序中获取设备/操作系统版本和国家/语言代码