"timer + Task.Run" 对比 "while loop + Task.Delay" 在 asp.net 核心托管服务中
"timer + Task.Run" vs "while loop + Task.Delay" in asp.net core hosted service
我有一个要求,后台服务应该 运行 Process
方法每天在 0:00 a.m.
因此,我的一位团队成员编写了以下代码:
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
,所以线程被阻塞并且不执行任何有用的工作(如果我错了请纠正我)。
我可以像这样让一个动作异步并在其中等待:
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
class 有两个隐藏的问题:
- 后续事件可能会以重叠方式调用附加的事件处理程序。
- 处理程序内部抛出的异常被吞没,此行为可能会在 .NET Framework 的未来版本中发生变化。(来自 docs)
您当前的 while
循环实现可以通过多种方式改进:
- 在
TimeSpan
计算期间多次读取 DateTime.Now
可能会产生意外结果,因为 DateTime.Now
返回的 DateTime
每次都可能不同。最好将 DateTime.Now
存储在变量中,并在计算中使用存储的值。
- 如果您还使用相同的标记作为
Task.Delay
的参数,则在 while
循环中检查条件 cancellationToken.IsCancellationRequested
可能会导致不一致的取消行为。完全跳过此检查更简单且一致。这样取消令牌将始终产生 OperationCanceledException
作为结果。
- 理想情况下,
Process
的持续时间不应影响下一个操作的调度。一种方法是在开始 Process
之前创建 Task.Delay
任务,并在 Process
完成后创建 await
它。或者您可以根据当前时间重新计算下一次延迟。这还有一个好处,即在 system-wise 时间更改的情况下,调度将自动调整。
这是我的建议:
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
的原因是为了防止非常剧烈的 system-wise 时间变化。
我有一个要求,后台服务应该 运行 Process
方法每天在 0:00 a.m.
因此,我的一位团队成员编写了以下代码:
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
,所以线程被阻塞并且不执行任何有用的工作(如果我错了请纠正我)。
我可以像这样让一个动作异步并在其中等待:
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
class 有两个隐藏的问题:
- 后续事件可能会以重叠方式调用附加的事件处理程序。
- 处理程序内部抛出的异常被吞没,此行为可能会在 .NET Framework 的未来版本中发生变化。(来自 docs)
您当前的 while
循环实现可以通过多种方式改进:
- 在
TimeSpan
计算期间多次读取DateTime.Now
可能会产生意外结果,因为DateTime.Now
返回的DateTime
每次都可能不同。最好将DateTime.Now
存储在变量中,并在计算中使用存储的值。 - 如果您还使用相同的标记作为
Task.Delay
的参数,则在while
循环中检查条件cancellationToken.IsCancellationRequested
可能会导致不一致的取消行为。完全跳过此检查更简单且一致。这样取消令牌将始终产生OperationCanceledException
作为结果。 - 理想情况下,
Process
的持续时间不应影响下一个操作的调度。一种方法是在开始Process
之前创建Task.Delay
任务,并在Process
完成后创建await
它。或者您可以根据当前时间重新计算下一次延迟。这还有一个好处,即在 system-wise 时间更改的情况下,调度将自动调整。
这是我的建议:
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
的原因是为了防止非常剧烈的 system-wise 时间变化。