我正在使用 .NET 的 HostBuilder
编写后台服务.
我有一门课叫 MyService
实现 BackgroundService
ExecuteAsync
方法,我在那里遇到了一些奇怪的行为。
里面方法我await
某个任务,以及在 await
之后抛出的任何异常被吞下,但在 await
之前抛出异常终止进程。
我在各种论坛(堆栈溢出、msdn、中等)上查看了在线信息,但找不到这种行为的解释。
public class MyService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(500, stoppingToken);
throw new Exception("oy vey"); // this exception will be swallowed
}
}
public class MyService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
throw new Exception("oy vey"); // this exception will terminate the process
await Task.Delay(500, stoppingToken);
}
}
我希望这两个异常都会终止该过程。
最佳答案
TL;博士;
不要让异常从 ExecuteAsync
.处理它们、隐藏它们或明确请求关闭应用程序。
在开始第一个异步操作之前不要等待太久
解释
这与 await
关系不大本身。之后抛出的异常将冒泡给调用者。处理它们的是调用者,或者不是。ExecuteAsync
是 BackgroundService
调用的方法这意味着该方法引发的任何异常都将由 BackgroundService
处理。 . That code is :
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)
{
// Stop called without start
if (_executingTask == null)
{
return;
}
try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
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()
RunAsync() Main() 中用于启动托管服务的方法实际上调用了主机的 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();
这意味着直到第一个真正的
await
在 BackgroundService
列表中对象在原始线程上运行。除非处理,否则任何扔在那里的东西都会导致应用程序崩溃。由于IHost.RunAsync()
或 IHost.StartAsync()
在 Main()
中调用,这就是 try/catch
应放置 block 。这也意味着在第一个真正的异步操作之前放置慢代码可能会延迟整个应用程序。
第一次异步操作之后的所有内容都将继续在线程池线程上运行。这就是为什么在第一次操作之后抛出的异常不会冒泡,直到通过调用
IHost.StopAsync
关闭托管服务。或任何孤立任务获得 GCd结论
不要让异常逃逸
ExecuteAsync
.捕获它们并妥善处理它们。选项是:ExecuteAsync
不会导致应用程序退出。 catch
堵塞。这将调用 StopAsync
也适用于所有其他后台服务文档
托管服务的行为和
BackgroundService
在 Implement background tasks in microservices with IHostedService and the BackgroundService class 中有描述和 Background tasks with hosted services in ASP.NET Core .文档没有解释如果其中一项服务抛出会发生什么。它们演示了具有显式错误处理的特定使用场景。 The queued background service example丢弃导致故障的消息并移至下一条:
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
关于c# - 如果在 'await' 之后抛出,则从任务中抛出的异常被吞没,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56871146/