我正在使用.net的HostBuilder编写后台服务。我有一个名为MyService的类,该类实现BackgroundService ExecuteAsync方法,并且在那里遇到了一些奇怪的行为。在方法内部,我等待某个任务,并且吞没了等待之后引发的任何异常,但是在等待终止过程之前引发的异常。
我在各种论坛(堆栈溢出,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;DR;
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
Explanation
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
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;
}
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.
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));
}
}
From Service to Host
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();
}
The interesting part is :
foreach (var hostedService in _hostedServices)
{
// Fire IHostedService.Start
await hostedService.StartAsync(cancellationToken).ConfigureAwait(false);
}
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.
From Host to Main()
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();
}
}
}
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();
or
await host.RunConsoleAsync();
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.
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
Conclusion
Don't let exceptions escape ExecuteAsync
. Catch them and handle them appropriately. The options are :
ExecuteAsync
doesn't cause the application to exit.catch
block. This will call StopAsync
on all other background services tooDocumentation
托管服务和行为BackgroundService
中描述实现与IHostedService和BackgroundService类微服务后台任务,并在ASP.NET核心托管服务后台任务。
该文档没有解释如果其中一项服务抛出该怎么办。他们通过明确的错误处理演示了特定的使用方案。排队的后台服务示例将丢弃导致故障的消息,并转到下一个消息:
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
惊人的解释!非常感谢。如果您有任何推荐的资源来阅读有关此主题的更多信息,我将不胜感激。
@TheDotFestClub不幸的是到源的链接,痛苦的经历。文档示例显示了如何创建BackgroundService,但没有说明其行为方式,也不解释了为什么这样编写示例。我浪费了很多时间在为什么我的应用程序
ExecuteAsync
正常完成后为何“手”徘徊-直到我意识到必须调用某个东西Stop
。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.
-但已处理异常。它由async
状态机捕获,并放在返回的上Task
。@StephenCleary我花了最后一个小时在Github中追踪代码。我什至不能再想了。我无法再继续追逐电话了。我发现其中一些困难的方法,而有些错误我刚刚通过代码了解到。该ExecuteAsync文件并没有说明例外,虽然任何事情。
@StephenCleary PS托管服务文章至少应分为3篇。它试图一次显示太多东西。结果太浅和太混乱了