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

时间:2019-07-03 13:20:12

标签: c# .net-core async-await task-parallel-library background-service

我正在使用.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);
        }
    }

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

3 个答案:

答案 0 :(得分:5)

TL; DR;

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

在该处开始第一个异步操作之前也不要等待太久

说明

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

ExecuteAsyncBackgroundService调用的方法,这意味着该方法引发的任何异常都将由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的检查是一种优化,可以避免在任务已经完成的情况下创建异步基础结构。

在调用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方法由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();
    }

有趣的部分是:

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

直到第一个真正的异步操作的所有代码都在原始线程上运行。遇到第一个异步操作时,将释放原始线程。该任务完成后,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();
            }

        }
    }

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

await host.RunAsync();

await host.RunConsoleAsync();

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

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

第一个异步操作之后的所有 都将继续在线程池线程上运行。这就是为什么在第一个操作之后在 之后引发异常的原因,除非通过调用IHost.StopAsync关闭托管服务或任何孤立的任务获得GCd

结论

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

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

文档

Implement background tasks in microservices with IHostedService and the BackgroundService classBackground tasks with hosted services in ASP.NET Core中描述了托管服务和BackgroundService的行为。

文档没有解释如果这些服务之一抛出该怎么办。他们通过明确的错误处理演示了特定的使用方案。 The queued background service example丢弃导致故障的消息,然后移至下一条消息:

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

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

答案 1 :(得分:1)

您不必使用 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);
    }
}

答案 2 :(得分:0)

简短答案

您不必等待Task方法返回的ExecuteAsync。如果您等待,您将在第一个示例中观察到异常。

长答案

这是关于“被忽略”的任务以及该异常的传播时间。

首先是等待之前的异常立即传播的原因。

Task DoSomethingAsync()
{
    throw new Exception();
    await Task.Delay(1);
}

在您从其调用上下文的情况下,await语句之前的部分将同步执行。堆栈保持不变。这就是为什么您在呼叫站点上观察到异常的原因。现在,您没有对此异常执行任何操作,因此它终止了您的进程。

在第二个示例中:

Task DoSomethingAsync()
{
    await Task.Delay(1);
    throw new Exception();
}

编译器已制作了涉及延续的样板代码。因此,您调用方法DoSomethingAsync。该方法立即返回。您不必等待,因此您的代码会立即继续。样板文件继续了await语句下面的代码行。这种延续将被称为“不是您的代码的东西”,并且将获得异常,并封装在异步任务中。现在,该任务将不会执行任何操作,直到将其解开为止。

无法观察的任务想让某人知道出了点问题,因此终结器有一个窍门。如果未观察到任务,则终结器将引发异常。因此,在这种情况下,任务可以传播其异常的第一点是在完成垃圾回收之前将其终止。

您的进程不会立即崩溃,但是它将在任务被垃圾回收之前崩溃。

阅读材料: