c# - asp.net 核心托管服务中的“计时器 + Task.Run”与“while 循环 + Task.Delay”
问题描述
我要求后台服务应该在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;
}
}
此代码按预期工作。但我不喜欢它同步等待直到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 操作(查询数据库并插入一些数据),并且不建议Task.Run
在 ASP.NET 环境中使用。
我重构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
该类有两个隐藏的陷阱:
- 后续事件可能会以重叠的方式调用附加的事件处理程序。
- 处理程序中抛出的异常会被吞没,并且此行为可能会在 .NET Framework 的未来版本中发生变化。(来自文档)
您当前的while
循环实现可以通过各种方式进行改进:
DateTime.Now
在计算过程中多次读取TimeSpan
可能会产生意想不到的结果,因为每次DateTime
返回的结果可能不同。DateTime.Now
最好将 存储DateTime.Now
在变量中,并在计算中使用存储的值。- 检查循环
cancellationToken.IsCancellationRequested
中的条件while
可能会导致不一致的取消行为,如果您还使用相同的标记作为Task.Delay
. 完全跳过此检查更简单且一致。这样取消令牌总是会产生OperationCanceledException
一个结果。 - 理想情况下,持续时间
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;
}
}
这样做的原因minimumIntervalBetweenStarts
是为了防止非常剧烈的系统时间变化。
推荐阅读
- c - c编程中的文字和字符之间有什么区别吗?
- python - python-libtorrent torrent_info 方法
- flutter - 如何在颤动中禁用滚动飞溅
- python - 谁能帮我找出 spyder (Anaconda) 中的错误
- image - PPM 文件打不开
- android - 在 Kotlin 中使用 contains() 的搜索系统以及与字符串和列表的结果差异
- python - 在 Python (pandas) 中填写缺失的日期和时间
- mocha.js - before(..) 块中的 cy.visit() 导致 CypressError:无法在正在运行的测试之外调用 cy.visit()
- python - PyCharm 处理本地包的方式不同吗?
- java - 在 Java 中运行线程时更改线程优先级的条件是什么?