“计时器+ Task.Run";对比"while循环+ Task.Delay"在asp.net核心托管服务中 [英] "timer + Task.Run" vs "while loop + Task.Delay" in asp.net core hosted service

查看:47
本文介绍了“计时器+ Task.Run";对比"while循环+ Task.Delay"在asp.net核心托管服务中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我要求后台服务每天早上0:00运行 Process 方法.

I have a requirement that background service should run Process method every day at 0:00 a.m.

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

So, one of my team member wrote the following code:

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 ,所以线程被阻塞并且没有执行任何有用的工作(如果我错了,请纠正我).

This code works as expected. But I don't like that it synchronously waits till calling Process first time, so a thread is blocked and not performing any useful work (correct me if I am wrong).

我可以像这样使动作异步并在其中等待:

I could make an action async and await in it like this:

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 .

But I am not sure that using Task.Run is a good thing here as Process method should perform some I/O operations (query DB and insert some data), and because it's not recommended to use Task.Run in ASP.NET environment.

我重构了 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 中使用计时器.

And this allows me not to use timer in MyBackgroundService at all.

我应该对"timer + Task.Run"使用第一种方法吗?或第二个带有"while循环+ Task.Delay"?

Should I use the first approach with "timer + Task.Run" or the second one with "while loop + Task.Delay"?

推荐答案

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

The while loop approach is simpler and safer. Using the Timer class has two hidden gotchas:

  1. 随后的事件可能会以隐蔽的方式调用附加的事件处理程序.
  2. 处理程序中引发的异常被吞咽,并且此行为在.NET Framework的未来版本中可能会发生变化.(来自您当前的 while 循环实现可以通过各种方式进行改进:

    Your current while loop implementation can be improved in various ways though:

    1. TimeSpan 计算期间多次读取 DateTime.Now 可能会产生意外结果,因为 DateTime返回的 DateTime .现在可以每次都不同.最好将 DateTime.Now 存储在变量中,并在计算中使用存储的值.
    2. 如果您还使用相同的令牌作为 Task的参数,则在 while 循环中检查条件 cancellationToken.IsCancellationRequested 可能导致取消行为不一致.延迟.完全跳过此检查更为简单且一致.这样,取消令牌将始终产生 OperationCanceledException 作为结果.
    3. 理想情况下, Process 的持续时间不应影响下一个操作的调度.一种方法是在启动 Process 之前创建 Task.Delay 任务,并在完成后 await 处理.或者,您可以根据当前时间重新计算下一个延迟.这样做的好处还在于,在系统时间更改的情况下,可以自动调整计划.
    1. Reading the DateTime.Now multiple times during a TimeSpan calculation may produce unexpected results, because the DateTime returned by DateTime.Now can be different each time. It is preferable to store the DateTime.Now in a variable, and use the stored value in the calculations.
    2. Checking the condition cancellationToken.IsCancellationRequested in the while loop could result to inconsistent cancellation behavior, if you also use the same token as an argument of the Task.Delay. Skipping this check completely is simpler and consistent. This way cancelling the token will always produce an OperationCanceledException as a result.
    3. Ideally the duration of the Process should not affect the scheduling of the next operation. One way to do it is to create the Task.Delay task before starting the Process, and await it after the completion of the Process. Or you can just recalculate the next delay based on the current time. This has also the advantage that the scheduling will be adjusted automatically in case of a system-wise time change.

    这是我的建议:

    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 的原因是为了防止系统时间发生剧烈变化.

    The reason for the minimumIntervalBetweenStarts is to protect from very dramatic system-wise time changes.

    这篇关于“计时器+ Task.Run";对比"while循环+ Task.Delay"在asp.net核心托管服务中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆