任务抛出的异常被吞下,如果在“等待"之后抛出 [英] Exception thrown from task is swallowed, if thrown after 'await'
问题描述
我正在使用 .NET 的 HostBuilder
编写后台服务.我有一个名为 MyService
的类,它实现了 BackgroundService
ExecuteAsync
方法,我在那里遇到了一些奇怪的行为.在方法内部我await
某个任务,在await
之后抛出的任何异常都被吞了,但是在await
终止之前抛出的异常过程.
I'm writing a background service using .NET's HostBuilder
.
I have a class called MyService
that implements BackgroundService
ExecuteAsync
method, and I encountered some weird behavior there.
Inside the method I await
a certain task, and any exception thrown after the await
is swallowed, but an exception that is thrown before the await
terminates the process.
我在各种论坛(堆栈溢出、msdn、medium)上在线查看,但找不到对这种行为的解释.
I looked online in all sorts of forums (stack overflow, msdn, medium) but I could not find an explanation for this behavior.
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);
}
}
我希望这两个异常都会终止进程.
I expect both exception to terminate the process.
推荐答案
TL;DR;
不要让异常脱离 ExecuteAsync
.处理它们、隐藏它们或明确请求关闭应用程序.
Don't let exceptions get out of ExecuteAsync
. Handle them, hide them or request an application shutdown explicitly.
在开始第一个异步操作之前不要等待太久
Don't wait too long before starting the first asynchronous operation in there either
说明
这与 await
本身关系不大.在它之后抛出的异常会冒泡给调用者.是调用者处理它们,或者不处理它们.
This has little to do with await
itself. Exceptions thrown after it will bubble up to the caller. It's the caller that handles them, or not.
ExecuteAsync
是由 BackgroundService
调用的方法,这意味着该方法引发的任何异常都将由 BackgroundService
处理.该代码是 :
ExecuteAsync
is a method called by BackgroundService
which means any exception raised by the method will be handled by 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
是一种优化,可避免在任务已完成时创建异步基础结构.
Nothing awaits the returned task, so nothing is going to throw here. The check for IsCompleted
is an optimization that avoids creating the async infrastructure if the task is already complete.
在StopAsync 被调用.届时将抛出任何异常.
The task won't be checked again until StopAsync is called. That's when any exceptions will be thrown.
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 方法.代码揭示了发生了什么:
In turn, the StartAsync
method of each service is called by the StartAsync method of the Host implementation. The code reveals what's going on :
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
之后的所有内容将在该任务完成后恢复.
All the code up to the first real asynchronous operation runs on the original thread. When the first asynchronous operation is encountered, the original thread is released. Everything after the await
will resume once that task completes.
从主机到主()
The RunAsync() method used in Main() to start the hosted services actually calls the Host's StartAsync but not 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() 调用:
This means that any exceptions thrown inside the chain from RunAsync to just before the first async operation will bubble up to the Main() call that starts the hosted services :
await host.RunAsync();
或
await host.RunConsoleAsync();
这意味着直到 BackgroundService
对象列表中的 first 真正的 await
之前的所有内容都在原始线程上运行.除非处理,否则抛出的任何内容都会导致应用程序崩溃.由于 IHost.RunAsync()
或 IHost.StartAsync()
在 Main()
中被调用,这就是 try/catch
块应该被放置.
This means that everything up to the first real await
in the list of BackgroundService
objects runs on the original thread. Anything thrown there will bring down the application unless handled. Since the IHost.RunAsync()
or IHost.StartAsync()
are called in Main()
, that's where the try/catch
blocks should be placed.
这也意味着将慢代码放在第一个真正的异步操作之前可能会延迟整个应用程序.
This also means that putting slow code before the first real asynchronous operation could delay the entire application.
在第一个异步操作之后的所有内容都将继续在线程池线程上运行.这就是为什么在第一次操作之后抛出的异常不会冒泡,直到托管服务通过调用 IHost.StopAsync
关闭或任何孤立的任务获得 GCd
Everything after that first asynchronous operation will keep running on a threadpool thread. That's why exceptions thrown after that first operation won't bubble up until either the hosted services shut down by calling IHost.StopAsync
or any orphaned tasks get GCd
结论
不要让异常逃脱ExecuteAsync
.抓住它们并适当地处理它们.选项是:
Don't let exceptions escape ExecuteAsync
. Catch them and handle them appropriately. The options are :
- 记录并忽略"它们.这将使 BackgroundService 无法运行,直到用户或其他一些事件调用应用程序关闭.退出
ExecuteAsync
不会导致应用程序退出. - 重试该操作.这可能是简单服务中最常见的选项.
- 在排队或定时服务中,丢弃出现故障的消息或事件并移至下一个.这可能是最有弹性的选择.可以检查错误消息、将其移至死信"队列、重试等.
- 明确要求关机.为此,请添加 IHostedApplicationLifetTime 接口作为依赖项并调用 StopAsync 来自
catch
块.这也会在所有其他后台服务上调用StopAsync
- Log and "ignore" them. This will live the BackgroundService inoperative until either the user or some other event calls for an application shutdown. Exiting
ExecuteAsync
doesn't cause the application to exit. - Retry the operation. That's probably the most common option of a simple service.
- In a queued or timed service, discard the message or event that faulted and move to the next one. That's probably the most resilient option. The faulty message can be inspected, moved to a "dead letter" queue, retried etc.
- Explicitly ask for a shutdown. To do that, add the IHostedApplicationLifetTime interface as a dependency and call StopAsync from the
catch
block. This will callStopAsync
on all other background services too
文档
托管服务和 BackgroundService
的行为在 使用 IHostedService 和 BackgroundService 类在微服务中实现后台任务 和 在 ASP.NET Core 中使用托管服务的后台任务.
The behaviour of hosted services and BackgroundService
is described in Implement background tasks in microservices with IHostedService and the BackgroundService class and Background tasks with hosted services in ASP.NET Core.
文档没有解释如果这些服务之一抛出会发生什么.它们展示了具有显式错误处理的特定使用场景.排队后台服务示例 丢弃导致故障的消息并移动到下一个:
The docs don't explain what happens if one of those services throws. They demonstrate specific use scenarios with explicit error handling. The queued background service example discards the message that caused the fault and moves to the next one :
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
这篇关于任务抛出的异常被吞下,如果在“等待"之后抛出的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!