TL;DR;
在ExecuteAsync
中,不要让异常逃脱出去。要么处理它们、隐藏它们,要么显式地请求应用程序关闭。同时,也不要等太久才开始第一个异步操作。
Explanation
这与await
本身无关。在await
之后抛出的异常会上升到调用者。是调用者处理或不处理异常。
ExecuteAsync
是由BackgroundService
调用的方法,这意味着该方法引发的任何异常都将由BackgroundService
处理。那段代码如下:
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Store the task we're executing
_executingTask = ExecuteAsync(_stoppingCts.Token);
// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted)
{
return _executingTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
返回的任务没有等待任何东西,因此这里不会抛出任何异常。对于IsCompleted
的检查是一种优化,如果任务已经完成,则避免创建异步基础结构。
在调用StopAsync之前,任务不会再次被检查。这时将抛出任何异常。
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
if (_executingTask == null)
{
return;
}
try
{
_stoppingCts.Cancel();
}
finally
{
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
从服务到主机
随后,每个服务的StartAsync
方法都由主机实现的StartAsync方法调用。代码揭示了正在发生的事情:
public async Task StartAsync(CancellationToken cancellationToken = default)
{
_logger.Starting();
await _hostLifetime.WaitForStartAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
_hostedServices = Services.GetService<IEnumerable<IHostedService>>();
foreach (var hostedService in _hostedServices)
{
// Fire IHostedService.Start
await hostedService.StartAsync(cancellationToken).ConfigureAwait(false);
}
// Fire IHostApplicationLifetime.Started
_applicationLifetime?.NotifyStarted();
_logger.Started();
}
有趣的部分是:
foreach (var hostedService in _hostedServices)
{
// Fire IHostedService.Start
await hostedService.StartAsync(cancellationToken).ConfigureAwait(false);
}
第一个真正的异步操作之前的所有代码都在原始线程上运行。当遇到第一个异步操作时,原始线程将被释放。在 await
之后的所有内容都将在该任务完成后恢复。
从主机到Main()
Main() 中使用的 RunAsync() 方法用于启动托管服务,实际上调用了主机的 StartAsync 方法,但不会调用 StopAsync :
public static async Task RunAsync(this IHost host, CancellationToken token = default)
{
try
{
await host.StartAsync(token);
await host.WaitForShutdownAsync(token);
}
finally
{
#if DISPOSE_ASYNC
if (host is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
#endif
{
host.Dispose();
}
}
}
这意味着在从RunAsync到第一个异步操作之前的链中抛出的任何异常都会冒泡到启动托管服务的Main()调用:
await host.RunAsync();
或者
await host.RunConsoleAsync();
这意味着,在BackgroundService
对象列表中的第一个真正的await
之前的所有内容都在原始线程上运行。在那里抛出的任何异常都会导致应用程序崩溃,除非被处理。由于IHost.RunAsync()
或IHost.StartAsync()
是在Main()
中调用的,因此应该在那里放置try/catch
块。
这也意味着,在第一个真正的异步操作之前放置缓慢的代码会延迟整个应用程序。
在第一个异步操作之后的所有操作将继续在线程池线程上运行。这就是为什么在第一个操作之后抛出的异常不会冒泡上升,直到托管服务通过调用IHost.StopAsync
关闭或任何孤立的任务被GCd。
结论
不要让异常逃脱ExecuteAsync
。捕获并适当处理它们。选项包括:
- 记录并“忽略”它们。这将使BackgroundService无法运行,直到用户或其他事件调用应用程序关闭。退出
ExecuteAsync
不会导致应用程序退出。
- 重试操作。这可能是简单服务的最常见选项。
- 在队列或定时服务中,丢弃导致故障的消息或事件,并移动到下一个。这可能是最具弹性的选项。可以检查错误的消息,将其移动到“死信”队列中,重试等。
- 显式请求关闭。为此,请将IHostedApplicationLifetTime接口作为依赖项添加,并从
catch
块中调用StopAsync。这将在所有其他后台服务上调用StopAsync
文档
托管服务和BackgroundService
的行为在使用IHostedService和BackgroundService类实现微服务中的后台任务和使用托管服务在ASP.NET Core中处理后台任务中进行了描述。
文档没有解释如果其中一个服务抛出异常会发生什么。它们演示具有显式错误处理的特定用例场景。排队后台服务示例丢弃导致故障的消息并转到下一条:
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
ExecuteAsync
? - Panagiotis Kanavos