Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Работа по принципу 'запустил и забыл' легко начать и легко упустить. При запуске асинхронной операции и удалении возвращаемого объекта Taskвы теряете видимость завершения, отмены и сбоев.
Большинство ошибок времени существования в асинхронном коде — это ошибки владения, а не ошибки компилятора. Конечный автомат async и его Task остаются активными, пока работа остается доступной через продолжения. Проблемы возникают, когда приложение больше не отслеживает работу.
Почему одноразовое действие вызывает ошибки срока существования
При запуске фоновой работы, не отслеживая ее, вы создаете три риска:
- Операция может завершиться ошибкой, и никто не наблюдает исключение.
- Процесс или хост могут завершить выполнение до окончания операции.
- Операция может существовать дольше объекта или области, предназначенной для её контроля.
Используйте fire-and-forget только в тех случаях, когда выполняемая работа действительно необязательна, и сбой является приемлемой возможностью.
Явное отслеживание фоновой деятельности
В этом примере определяется BackgroundTaskTracker, пользовательский вспомогательный класс, содержащий потокобезопасный словарь задач в процессе выполнения. При вызове Track он регистрирует ContinueWith продолжение задачи, которая удаляет задачу из словаря после завершения и логирует любые сбои. При вызове DrainAsync и Task.WhenAll, он вызывает каждую задачу, которая еще находится в словаре, и возвращает итоговую задачу.
public sealed class BackgroundTaskTracker
{
private readonly ConcurrentDictionary<int, Task> _inFlight = new();
public void Track(Task operationTask, string name)
{
int id = operationTask.Id;
_inFlight[id] = operationTask;
_ = operationTask.ContinueWith(completedTask =>
{
_inFlight.TryRemove(id, out _);
if (completedTask.IsFaulted)
{
Console.WriteLine($"{name} failed: {completedTask.Exception?.GetBaseException().Message}");
}
}, TaskScheduler.Default);
}
public Task DrainAsync()
{
Task[] snapshot = _inFlight.Values.ToArray();
return snapshot.Length == 0 ? Task.CompletedTask : Task.WhenAll(snapshot);
}
}
Public NotInheritable Class BackgroundTaskTracker
Private ReadOnly _inFlight As New ConcurrentDictionary(Of Integer, Task)()
Public Sub Track(operationTask As Task, name As String)
Dim id As Integer = operationTask.Id
_inFlight(id) = operationTask
Dim continuationTask As Task = operationTask.ContinueWith(Sub(completedTask)
Dim removedTask As Task = Nothing
_inFlight.TryRemove(id, removedTask)
If completedTask.IsFaulted Then
Console.WriteLine($"{name} failed: {completedTask.Exception.GetBaseException().Message}")
End If
End Sub,
TaskScheduler.Default)
End Sub
Public Function DrainAsync() As Task
Dim snapshot As Task() = _inFlight.Values.ToArray()
If snapshot.Length = 0 Then
Return Task.CompletedTask
End If
Return Task.WhenAll(snapshot)
End Function
End Class
В следующем примере используется BackgroundTaskTracker для запуска, наблюдения и очистки фоновой операции:
public static class FireAndForgetFix
{
public static async Task RunAsync(BackgroundTaskTracker tracker)
{
Task backgroundTask = Task.Run(async () =>
{
await Task.Delay(100);
throw new InvalidOperationException("Background operation failed.");
});
tracker.Track(backgroundTask, "Cache refresh");
try
{
await tracker.DrainAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Drain observed failure: {ex.GetBaseException().Message}");
}
}
}
Public Module FireAndForgetFix
Public Async Function RunAsync(tracker As BackgroundTaskTracker) As Task
Dim backgroundTask As Task = Task.Run(Async Function()
Await Task.Delay(100)
Throw New InvalidOperationException("Background operation failed.")
End Function)
tracker.Track(backgroundTask, "Cache refresh")
Try
Await tracker.DrainAsync()
Catch ex As Exception
Console.WriteLine($"Drain observed failure: {ex.GetBaseException().Message}")
End Try
End Function
End Module
Вы можете спросить: если DrainAsync только ждет выполнения той задачи, которую вы начали, почему бы не использовать await backgroundTask напрямую и полностью исключить трекер? Для одной задачи в одном методе можно. Средство отслеживания становится ценным при запуске задач из многих разных мест в течение всего времени существования компонента. Каждый вызывающий абонент передает свою задачу общему трекировщику, и один DrainAsync звонок при завершении работы ожидает всех из них, не зная, сколько было запущено или кто их начал. Средство отслеживания также применяет согласованную политику наблюдения за исключениями: каждая зарегистрированная задача получает одинаковую обработку записей о сбоях, поэтому никакое исключение не может остаться незамеченным, независимо от того, какой путь кода инициировал выполнение задачи.
Ниже перечислены три основных компонента отслеживаемого шаблона:
-
Назначьте задачу переменной — хранение ссылки
backgroundTaskпозволяет отслеживать. Задача, на которую вы не можете ссылаться, — это задача, которую вы не можете слить или наблюдать. -
Зарегистрируйтесь в трекере —
tracker.Trackприкрепляет продолжение ведения журнала ошибок и добавляет задачу в набор выполняемых задач. Любое исключение фоновой работы становится явным через это продолжение, а не исчезает бесшумно. -
Слив при завершении работы —
tracker.DrainAsyncожидает завершения всего, что все еще работает. Вызовите его перед выходом компонента или процесса, чтобы гарантировать, что работа в полете не прекращена в середине полета.
Последствия неконтролируемых систем "выстрелил и забыл"
Если вы откажетесь от возвращаемого Task объекта вместо отслеживания, создадите незаметный сбой.
public static class FireAndForgetPitfall
{
public static async Task RunAsync()
{
_ = Task.Run(async () =>
{
await Task.Delay(100);
throw new InvalidOperationException("Background operation failed.");
});
await Task.Delay(150);
Console.WriteLine("Caller finished without observing background completion.");
}
}
Public Module FireAndForgetPitfall
Public Async Function RunAsync() As Task
Dim discardedTask As Task = Task.Run(Async Function()
Await Task.Delay(100)
Throw New InvalidOperationException("Background operation failed.")
End Function)
Await Task.Delay(150)
Console.WriteLine("Caller finished without observing background completion.")
End Function
End Module
Три проблемы следуют за удалением задачи:
-
Тихие исключения —
InvalidOperationExceptionиз фоновой операции никогда не наблюдаются. Среда выполнения направляет её UnobservedTaskException при завершении, что является неопределённым и происходит слишком поздно, чтобы можно было обработать корректно. - Отсутствие координации завершения работы — вызывающий продолжает и выходит, не ожидая завершения операции. В кратковременном процессе или узле с временем ожидания завершения работы фоновая работа отменяется или теряется полностью.
- Нет видимости — без ссылки на задачу невозможно определить, выполнена ли операция успешно, не выполнена или по-прежнему запущена.
Технология "запустил и забыл" без обратной связи приемлема только в том случае, если выполняются все три следующих условия: работа действительно необязательна, сбой можно безопасно игнорировать, и операция завершается хорошо до завершения любого ожидаемого срока существования процесса. Запись некритического телеметрического пинга — это пример, где все эти условия выполняются.
Убедитесь, что право владения указано явно
Используйте одну из следующих моделей владения:
- Верните
Taskи требуйте от вызывающих его, чтобы они ожидали результатов. - Отслеживание фоновых задач в выделенной службе владельца.
- Используйте управляемую хостом фоновую абстракцию, чтобы хост управлял временем жизни.
Если работа должна продолжаться после возврата вызывающего абонента, явно передайте владение. Например, передайте задачу в средство отслеживания, которое регистрирует ошибки и участвует в завершении работы.
Визуализация исключений из фоновых задач
Опущенные задачи могут незаметно провалиться до завершения и обработки необнаруженного исключения. Это время недетерминировано и слишком поздно для обычной обработки запросов или рабочих процессов.
Прикрепите логику наблюдения, когда вы ставите фоновую работу в очередь. По крайней мере, регистрируйте ошибки в процессе. Рекомендуется предпочесть централизованный центр отслеживания, чтобы каждая операция в очереди следовала одной и той же политике.
Сведения о распространении исключений см. в разделе "Обработка исключений задач".
Согласование отмены и завершения работы
Привяжите фоновую работу к маркеру отмены, представляющему время существования приложения или операции. Во время завершения работы:
- Перестать принимать новую работу.
- Отмена сигнала.
- Ожидание отслеживаемых задач с ограниченным временем ожидания.
- Журнал неполных операций.
Этот поток обеспечивает прогнозируемость завершения работы и предотвращает частичные операции записи или потерянные операции.
Может ли GC собрать асинхронный метод до завершения работы?
Среда выполнения поддерживает асинхронную машину состояний в активном состоянии, пока на нее ссылаются продолжения выполнения. Обычно асинхронная операция, выполняющаяся в данный момент, не теряется из-за сборки мусора, воздействующей на сам автомат состояний.
Вы по-прежнему можете потерять корректность выполнения, если вы потеряете контроль над возвращённой задачей, освободите необходимые ресурсы слишком рано или позволите процессу завершиться раньше времени. Сосредоточьтесь на владении задачами и согласованном завершении работы.