[英]Exception thrown from task is swallowed, if thrown after 'await'
我正在使用 .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
處理。 該代碼是:
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()方法實際上調用了主機的 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()
IHost.StartAsync()
中調用,因此應放置try/catch
塊。
這也意味着在第一個真正的異步操作之前放置慢代碼可能會延遲整個應用程序。
第一個異步操作之后的所有內容都將繼續在線程池線程上運行。 這就是為什么在第一次操作之后拋出的異常不會冒泡的原因,直到托管服務通過調用IHost.StopAsync
關閉或任何孤立的任務獲得 GCd
結論
不要讓異常逃脫ExecuteAsync
。 抓住它們並適當地處理它們。 選項是:
ExecuteAsync
不會導致應用程序退出。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)}.");
}
}
您不必使用BackgroundService
。 顧名思義,它對於不是流程主要責任的工作很有用,其錯誤不應導致流程退出。
如果這不符合您的需要,您可以推出自己的IHostedService
。 我使用了下面的WorkerService
,它比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);
}
await 語句之前的部分同步執行,在您從中調用它的上下文中。 堆棧保持不變。 這就是您在調用站點上觀察異常的原因。 現在,您沒有對這個異常做任何事情,所以它終止了您的進程。
在第二個例子中:
Task DoSomethingAsync()
{
await Task.Delay(1);
throw new Exception();
}
編譯器制作了涉及延續的樣板代碼。 所以你調用方法DoSomethingAsync
。 該方法立即返回。 您不會等待它,因此您的代碼會立即繼續。 樣板文件延續了await
語句下方的代碼行。 該延續將被稱為“不是您的代碼的東西”,並將獲得異常,並包裝在異步任務中。 現在,該任務不會做任何事情,直到它被解開。
未觀察到的任務想讓某人知道出了什么問題,因此終結器中有一個技巧。 如果任務未被觀察到,終結器將拋出異常。 所以在這種情況下,任務可以傳播其異常的第一個點是它完成時,在它被垃圾收集之前。
您的進程不會立即崩潰,但它會在任務被垃圾收集之前“崩潰”。
閱讀材料:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.