如果在'await'之后抛出异常,则吞噬从任务抛出的异常 [英] Exception thrown from task is swallowed if thrown after 'await'

查看:129
本文介绍了如果在'await'之后抛出异常,则吞噬从任务抛出的异常的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用.net的HostBuilder编写后台服务。
我有一个名为MyService的类,该类实现BackgroundService ExecuteAsync方法,并且在那里遇到了一些奇怪的行为。
在方法内部,我等待一个特定的任务,等待状态被吞没后抛出的任何异常,但是等待状态终止之前抛出的异常。

I'm writing a background service using .net's HostBuilder. I have class called MyService that implements BackgroundService ExecuteAsync method and i encountered some weird behavior there. Inside the method i await a certain task and any exception thrown after the await is swallowed but an exception that is thrown before the await terminates the process.

I在各种论坛(堆栈溢出,msdn,中等)中都在线查看,但我找不到这种行为的解释。

I looked online in all sorts of forums (stack overflow, msdn, medium) but I could not find an explanation for this behavior.

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

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

I expect both exception to terminate the process

推荐答案

TL; DR;

Don'不要让异常脱离 ExecuteAsync 。处理它们,隐藏它们或显式请求关闭应用程序。

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

说明

这与 await 无关本身。它引发的异常将冒泡给调用者。是否由呼叫者处理。

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 BackgroundService 调用的方法该方法引发的任何异常将由 BackgroundService 处理。 该代码

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

没有唤醒返回的任务,所以什么也没有要扔到这里。 IsCompleted 的检查是一种优化,可以避免在任务已经完成的情况下创建异步基础结构。

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.

直到 StopAsync 被调用。届时将引发任何异常。

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

    }

从服务到主机

依次,每个服务的 StartAsync 方法由 StartAsync 方法。该代码揭示了正在发生的事情:

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

有趣的部分是:

        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.

从主机到Main()

RunAsync()方法实际上调用了主机的StartAsync,但不是 StopAsync:

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

        }
    }

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

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

await host.RunConsoleAsync();

这意味着直到 first 实数 BackgroundService 对象列表中的> await 在原始线程上运行。除非处理,否则抛出的任何内容都会使应用程序崩溃。由于 IHost.RunAsync() IHost.StartAsync() Main( ),也就是应该放置 try / catch 块的地方。

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.

之后的所有 异步操作将继续在线程池线程上运行。这就是为什么在之后引发的异常不会冒泡的现象,直到托管服务通过调用 IHost.StopAsync 关闭或任何孤立的任务获得GCd

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

结论

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

Don't let exceptions escape ExecuteAsync. Catch them and handle them appropriately. The options are :


  • 登录并忽略它们。这将使BackgroundService无法运行,直到用户或其他事件要求关闭应用程序为止。退出 ExecuteAsync 不会导致应用程序退出。

  • 重试该操作。这可能是简单服务中最常见的选择。

  • 在排队或定时服务中,丢弃出现故障的消息或事件,然后移至下一个。这可能是最灵活的选择。可以检查错误消息,将其移至死信队列,然后重试等。

  • 明确要求关机。为此,请添加 IHostedApplicationLifetTime 接口作为依赖项,并调用 StopAsync 来自 catch 块。这也会在所有其他后台服务上调用 StopAsync

  • Log and "ignore" them. This will live the BackgroundService inoperative until either the user or some other event calls for an application shutdown. Exiting ExecuteAsync doesn't cause the application to exit.
  • Retry the operation. That's probably the most common option of a simple service.
  • In a queued or timed service, discard the message or event that faulted and move to the next one. That's probably the most resilient option. The faulty message can be inspected, moved to a "dead letter" queue, retried etc.
  • Explicitly ask for a shutdown. To do that, add the IHostedApplicationLifetTime interface as a dependency and call StopAsync from the catch block. This will call StopAsync on all other background services too

文档

托管服务和 BackgroundService 的行为在使用IHostedService在微服务中实现后台任务和BackgroundService类在ASP.NET Core中具有托管服务的后台任务

The behaviour of hosted services and BackgroundService is described in Implement background tasks in microservices with IHostedService and the BackgroundService class and Background tasks with hosted services in ASP.NET Core.

文档没有说明如果其中之一这些服务抛出。他们通过明确的错误处理演示了特定的使用方案。 排队的后台服务示例丢弃导致故障的消息,然后移至下一个消息:

The docs don't explain what happens if one of those services throws. They demonstrate specific use scenarios with explicit error handling. The queued background service example discards the message that caused the fault and moves to the next one :

    while (!cancellationToken.IsCancellationRequested)
    {
        var workItem = await TaskQueue.DequeueAsync(cancellationToken);

        try
        {
            await workItem(cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, 
               $"Error occurred executing {nameof(workItem)}.");
        }
    }

这篇关于如果在'await'之后抛出异常,则吞噬从任务抛出的异常的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆