如果在'await'之后抛出异常,则吞噬从任务抛出的异常

The*_*lub 3 c# background-service task-parallel-library async-await .net-core

我正在使用.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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

我希望这两个异常都会终止该过程

Pan*_*vos 6

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;
    }
Run Code Online (Sandbox Code Playgroud)

什么都没有等待返回的任务,因此这里什么也不会丢。检查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));
        }

    }
Run Code Online (Sandbox Code Playgroud)

从服务到主机

反过来,StartAsync每个服务的方法都由Host实现的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();
    }
Run Code Online (Sandbox Code Playgroud)

有趣的部分是:

        foreach (var hostedService in _hostedServices)
        {
            // Fire IHostedService.Start
            await hostedService.StartAsync(cancellationToken).ConfigureAwait(false);
        }
Run Code Online (Sandbox Code Playgroud)

直到第一个真正的异步操作的所有代码都在原始线程上运行。遇到第一个异步操作时,将释放原始线程。await该任务完成后,一切将恢复。

从主机到Main()

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();
            }

        }
    }
Run Code Online (Sandbox Code Playgroud)

这意味着从RunAsync到第一个异步操作之前的链内抛出的所有异常都将冒泡到启动托管服务的Main()调用中:

await host.RunAsync();
Run Code Online (Sandbox Code Playgroud)

要么

await host.RunConsoleAsync();
Run Code Online (Sandbox Code Playgroud)

这意味着直到对象列表中第一个实数的所有内容都在原始线程上运行。除非处理,否则抛出的任何内容都会使应用程序崩溃。由于或调用在中,因此应该放置块。awaitBackgroundServiceIHost.RunAsync()IHost.StartAsync()Main()try/catch

这也意味着,把慢的代码之前的第一个真正的异步操作可能会延迟整个应用程序。

第一个异步操作之后的所有内容将继续在线程池线程上运行。因此,第一个操作之后引发的异常不会冒泡,直到托管服务通过调用关闭IHost.StopAsync或任何孤立的任务获得GCd为止

结论

不要让异常逃脱ExecuteAsync。抓住并妥善处理。选项是:

  • 记录并“忽略”它们。这将使BackgroundService无法运行,直到用户或其他事件要求关闭应用程序为止。退出ExecuteAsync不会导致应用程序退出。
  • 重试该操作。这可能是简单服务的最常见选择。
  • 在排队或定时服务中,丢弃出现故障的消息或事件,然后移至下一个。那可能是最有弹性的选择。可以检查错误消息,将其移至“死信”队列,然后重试等。
  • 明确要求关机。为此,添加IHostedApplicationLifetTime接口作为依赖项,然后catch块中调用StopAsync。这也将调用StopAsync所有其他后台服务

文献资料

托管服务和行为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)}.");
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • 惊人的解释!多谢。如果您有任何推荐的资源来阅读有关该主题的更多信息,我将非常感激。 (3认同)
  • @StephenCleary 我花了最后一个小时在 Github 上追踪代码。我什至不能再想了。我*不能*再次开始追寻电话。其中一些事情是我通过困难的方式发现的,一些失败是我刚刚通过代码才理解的。不过, [ExecuteAsync](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice.executeasync?view=aspnetcore-3.0) 文档没有说明任何有关异常的信息。 (3认同)
  • @StephenCleary PS 托管服务文章应该分为至少 3 篇不同的文章。它试图同时展示太多的东西。它最终既太浅薄又太混乱 (2认同)