如果从任务中抛出的异常在"等待"之后抛出,则会被吞噬



我正在使用.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本身无关。例外将在呼叫者身上弹出之后。是否处理它们是呼叫者

ExecuteAsyncBackgroundService调用的方法,这意味着该方法提出的任何例外将由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块的位置。

这也意味着将慢速代码放在之前第一个真实的异步操作可能会延迟整个应用程序。

所有内容 第一个异步操作将继续在线程池螺纹上运行。这就是为什么在之后引发的例外,直到托管服务通过调用IHost.StopAsync或任何孤立任务关闭的托管服务都不会起泡,或

结论

不要让异常逃脱ExecuteAsync。抓住它们并适当处理它们。选项是:

  • 日志和"忽略"它们。这将使背景服务不起作用,直到用户或其他某些事件要求应用程序关闭为止。退出ExecuteAsync不会导致申请退出。
  • 重试操作。这可能是简单服务的最常见选择。
  • 在排队或定时服务中,丢弃了错误的消息或事件,然后移至下一个。这可能是最有弹性的选择。可以检查错误的消息,转移到"死信"队列,重述等。
  • 明确要求关闭。为此,请将iHostedApplicationLifettime接口添加为依赖关系,并从catch块中调用StopAsync。这也将在所有其他背景服务上调用StopAsync

文档

在用iHostedService的微服务中介绍了托管服务和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#
  • 中解剖异步方法

相关内容

最新更新