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 реализации Host. Код показывает, что происходит:
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
, возобновится после завершения этой задачи.
От хоста к основному ()
Метод As используется в Main () для запуска размещенных служб, на самом деле вызывает StartAsync Host, но не 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();
or
await host.RunConsoleAsync();
Это означает, что все, вплоть до первого реального 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)}.");
}
}
person
Panagiotis Kanavos
schedule
03.07.2019
ExecuteAsync
? - person Panagiotis Kanavos   schedule 03.07.2019RunAsync
,StartAsync
? - person Panagiotis Kanavos   schedule 03.07.2019ExecuteAsync
вызывается из базового класса, который является кодом .net. Вызывается, но не ожидается вBackgroundService.StartAsync
- person TheDotFestClub   schedule 03.07.2019RunConsoleAsync
- person TheDotFestClub   schedule 03.07.2019