我正在使用.NET的HostBuilder
编写背景服务。我有一个称为MyService
的类,该类实现BackgroundService
ExecuteAsync
方法,在那里我遇到了一些奇怪的行为。在方法内i 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; dr;
不要让异常从ExecuteAsync
出来。处理它们,隐藏它们或明确要求应用程序关闭。
不要等待太久才开始在那里开始第一次异步操作
说明
这与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)
{
// 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((
在main((启动托管服务中使用的RunAsync((方法实际上调用了主机的startasync,但 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((呼叫:
await host.RunAsync();
或
await host.RunConsoleAsync();
这意味着BackgroundService
对象列表中的所有内容均在原始线程上运行。除非处理,否则在那里抛出的任何东西都会放下申请。由于IHost.RunAsync()
或IHost.StartAsync()
在Main()
中调用,因此应放置try/catch
块的位置。
这也意味着将慢速代码放在之前第一个真实的异步操作可能会延迟整个应用程序。
所有内容 第一个异步操作将继续在线程池螺纹上运行。这就是为什么在之后引发的例外,直到托管服务通过调用 结论 不要让异常逃脱 文档 在用iHostedService的微服务中介绍了托管服务和 文档没有解释如果其中一种服务抛出会发生什么。他们演示了具有明确的错误处理的特定使用方案。排队的背景服务示例丢弃了导致故障并移至下一个消息的消息:IHost.StopAsync
或任何孤立任务关闭的托管服务都不会起泡,或ExecuteAsync
。抓住它们并适当处理它们。选项是:ExecuteAsync
不会导致申请退出。catch
块中调用StopAsync。这也将在所有其他背景服务上调用StopAsync
BackgroundService
的行为,以及带有ASP.NET Core的托管服务的MicroServices以及BackgroundService类和背景任务。 while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
您不必使用BackgroundService
。顾名思义,这对于工作不是该过程的主要责任并且其错误不应导致其退出的工作很有用。
如果这不符合您的需求,则可以滚动自己的IHostedService
。我使用了以下WorkerService
,该 CC_30比IApplicationLifetime.StopApplication()
具有一些优势。由于async void
在线程池上运行连续性,因此可以使用AppDomain.CurrentDomain.UnhandledException
处理错误,并使用错误退出代码终止。有关更多详细信息,请参见XML评论。
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
namespace MyWorkerApp.Hosting
{
/// <summary>
/// Base class for implementing a continuous <see cref="IHostedService"/>.
/// </summary>
/// <remarks>
/// Differences from <see cref="BackgroundService"/>:
/// <list type = "bullet">
/// <item><description><see cref="ExecuteAsync"/> is repeated indefinitely if it completes.</description></item>
/// <item><description>Unhandled exceptions are observed on the thread pool.</description></item>
/// <item><description>Stopping timeouts are propagated to the caller.</description></item>
/// </list>
/// </remarks>
public abstract class WorkerService : IHostedService, IDisposable
{
private readonly TaskCompletionSource<byte> running = new TaskCompletionSource<byte>();
private readonly CancellationTokenSource stopping = new CancellationTokenSource();
/// <inheritdoc/>
public virtual Task StartAsync(CancellationToken cancellationToken)
{
Loop();
async void Loop()
{
if (this.stopping.IsCancellationRequested)
{
return;
}
try
{
await this.ExecuteAsync(this.stopping.Token);
}
catch (OperationCanceledException) when (this.stopping.IsCancellationRequested)
{
this.running.SetResult(default);
return;
}
Loop();
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public virtual Task StopAsync(CancellationToken cancellationToken)
{
this.stopping.Cancel();
return Task.WhenAny(this.running.Task, Task.Delay(Timeout.Infinite, cancellationToken)).Unwrap();
}
/// <inheritdoc/>
public virtual void Dispose() => this.stopping.Cancel();
/// <summary>
/// Override to perform the work of the service.
/// </summary>
/// <remarks>
/// The implementation will be invoked again each time the task completes, until application is stopped (or exception is thrown).
/// </remarks>
/// <param name="cancellationToken">A token for cancellation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
protected abstract Task ExecuteAsync(CancellationToken cancellationToken);
}
}
简短答案
您不在等待从ExecuteAsync
方法返回的Task
。如果您要等待它,您会观察到第一个示例中的例外。
长答案
所以这是关于"忽略"任务,当例外传播时。
首先是等待立即传播之前异常的原因。
Task DoSomethingAsync()
{
throw new Exception();
await Task.Delay(1);
}
等待语句之前的零件同步执行,在您从您呼叫的上下文中。堆栈保持完整。这就是为什么您在呼叫网站上观察例外的原因。现在,您没有在此例外做任何事情,因此它终止了您的流程。
在第二个示例中:
Task DoSomethingAsync()
{
await Task.Delay(1);
throw new Exception();
}
编译器制作了涉及延续的样板代码。因此,您调用方法DoSomethingAsync
。该方法立即返回。您不等待它,因此您的代码会立即继续。样板已延续到await
语句下方的代码线。该延续将被称为"不是您的代码的东西",并将得到异常,并包裹在异步任务中。现在,该任务在未包装之前不会执行任何操作。
未观察到的任务想让某人知道某些问题出了问题,因此最终制度中有一个技巧。如果任务未观察到,最终制度将抛出例外。因此,在这种情况下,任务可以传播其异常的第一个点是在收集垃圾之前完成其最终确定时。
您的过程不会立即崩溃,但是在收集垃圾之前,它将崩溃。
阅读材料:
- 最终化器
- 任务和未手动异常
- 在c# 中解剖异步方法