Исключение, выбрасываемое из задания, проглатывается, если выброшено после 'await' - PullRequest
2 голосов
/ 03 июля 2019

Я пишу фоновый сервис, используя .net HostBuilder. У меня есть класс под названием MyService, который реализует метод BackgroundService ExecuteAsync, и я столкнулся с некоторым странным поведением там. Внутри метода я жду определенной задачи и любое исключение, выданное после того, как ожидание проглочено, но исключение, которое выдается до того, как ожидание завершает процесс.

Я смотрел онлайн на всех форумах (переполнение стека, msdn, medium), но не смог найти объяснения этому поведению.

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

Я ожидаю, что оба исключения завершат процесс

Ответы [ 2 ]

5 голосов
/ 03 июля 2019

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

Ничего не ждет возвращенного задания, поэтому здесь ничего не будет выброшено.Проверка для 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 () , используемый вMain () для запуска размещенных служб фактически вызывает 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();

Это означает, что все до first real await в списке BackgroundService объектов запускается в исходном потоке.Все, что там будет брошено, приведет к падению приложения, если не будет выполнено.Поскольку IHost.RunAsync() или IHost.StartAsync() вызываются в Main(), именно здесь должны быть размещены блоки try/catch.

Это также означает, что медленный код перед первым реальнымасинхронная операция может задержать все приложение.

Все после , что первая асинхронная операция будет продолжать выполняться в потоке пула потоков.Вот почему исключения выдают после , что первая операция не будет пузыриться до тех пор, пока хост-службы не закроются с помощью вызова IHost.StopAsync или какие-либо потерянные задачи получат GCd

Заключение

Не допускайте исключения исключений ExecuteAsync.Лови их и обращайся с ними соответствующим образом.Возможные варианты:

  • Записывать и «игнорировать» их.Это будет продолжать работать с BackgroundService до тех пор, пока пользователь или какое-либо другое событие не вызовет завершение работы приложения.Выход ExecuteAsync не приводит к закрытию приложения.
  • Повторите операцию.Вероятно, это наиболее распространенный вариант простого сервиса.
  • В сервисе, поставленном в очередь или по времени, отбросьте сообщение или событие, вызвавшее сбой, и перейдите к следующему.Это, наверное, самый устойчивый вариант.Неисправное сообщение можно проверить, переместить в очередь «мертвых писем», повторить попытку и т. Д.
  • Явно запросить завершение работы.Для этого добавьте в качестве зависимости интерфейс IHostedApplicationLifetTime и вызовите StopAsync из блока catch.Это вызовет 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)}.");
        }
    }
0 голосов
/ 03 июля 2019

Короткий ответ

Вы не ожидаете Task, который возвращается методом ExecuteAsync.Если бы вы ожидали этого, вы бы заметили исключение из вашего первого примера.

Длинный ответ

Таким образом, речь идет о «игнорируемых» задачах и при распространении этого исключения.

Первая причина, по которой исключение перед ожиданием распространяется мгновенно.

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

Часть перед оператором await выполняется синхронно в контексте, из которого вы ее вызывали.Стек остается нетронутым.Вот почему вы наблюдаете исключение на сайте вызова.Теперь вы ничего не сделали с этим исключением, поэтому оно завершает ваш процесс.

Во втором примере:

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

Компилятор создал шаблонный код, который включает продолжение.Итак, вы называете метод DoSomethingAsync.Метод возвращается мгновенно.Вы не ждете этого, поэтому ваш код продолжается мгновенно.В шаблоне было продолжение строки кода под оператором await.Это продолжение будет называться «что-то, что не является вашим кодом» и получит исключение, заключенное в асинхронную задачу.Теперь эта задача ничего не будет делать, пока она не будет развернута.

Ненаблюдаемые задачи хотят, чтобы кто-то знал, что что-то пошло не так, поэтому в финализаторе есть хитрость.Финализатор выдаст исключение, если задача не была замечена.Таким образом, в этом случае первая точка, в которой задача может распространить свое исключение, - это когда она завершена, до того, как будет собрана сборка мусора.

Ваш процесс не завершается мгновенно, но он завершится «до» выполнения задачи.Сборщик мусора.

Материал для чтения:

...