Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Если библиотека предоставляет только асинхронные API, потребители иногда упаковывают их в синхронные вызовы для удовлетворения синхронного интерфейса или контракта. Этот шаблон 'sync-over-async' может показаться простым, но это распространенный источник взаимоблокировок и проблем с производительностью.
Основные шаблоны упаковки
Синхронная оболочка вокруг метода асинхронного шаблона на основе задач (TAP) обращается к свойству задачи Result , которое блокирует вызывающий поток:
public class TapWrapper
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
}
Public Module TapWrapper
Public Function Foo(fooAsync As Func(Of Task(Of Integer))) As Integer
Return fooAsync().Result
End Function
End Module
Такой подход выглядит просто, но может вызвать серьезные проблемы в зависимости от среды, в которой она выполняется.
Взаимоблокировка в однопоточных контекстах
Самый опасный сценарий возникает при вызове синхронной оболочки из потока, который является однопоточным SynchronizationContext. Этот сценарий обычно представляет собой поток пользовательского интерфейса в приложениях WPF, Windows Forms или MAUI.
public static class DeadlockExample
{
private static void Delay(int milliseconds)
{
DelayAsync(milliseconds).Wait();
}
private static async Task DelayAsync(int milliseconds)
{
await Task.Delay(milliseconds);
}
}
Public Module DeadlockExample
Private Sub Delay(milliseconds As Integer)
DelayAsync(milliseconds).Wait()
End Sub
Private Async Function DelayAsync(milliseconds As Integer) As Task
Await Task.Delay(milliseconds)
End Function
End Module
Вот что происходит шаг за шагом.
- Поток пользовательского интерфейса вызывает
Delay, который вызываетDelayAsync(milliseconds).Wait(). -
DelayAsyncвыполняется синхронно до тех пор, пока он не достигнетawait Task.Delay(milliseconds). - Так как задержка еще не завершена,
awaitфиксирует текущий SynchronizationContext и приостанавливается.DelayAsyncвозвращает Task вызывающему. - Поток пользовательского интерфейса блокируется,
.Wait()ожидая завершения этой задачи. - По завершении задержки продолжение должно выполняться в исходном
SynchronizationContextпотоке пользовательского интерфейса. - Поток пользовательского интерфейса не может обработать продолжение, так как он заблокирован в
.Wait(). - Взаимоблокировка.
Это важно
Успех или неудача кода sync-over-async зависит от среды, в которой он выполняется. Код, который работает в консольном приложении, может привести к взаимоблокировке на потоке пользовательского интерфейса или в ASP.NET (на платформе .NET Framework). Эта зависимость от среды является основной причиной, чтобы избегать раскрытия синхронных оболочек.
Истощение пула потоков
Взаимоблокировки не ограничиваются потоками пользовательского интерфейса. Если асинхронный метод зависит от пула потоков для завершения своей работы, например, посредством постановки в очередь заключительного этапа обработки, блокировка многих потоков пула синхронными оболочками может истощить пул.
public static class ThreadPoolDeadlockExample
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
public static async Task DemonstrateDeadlockRiskAsync()
{
var tasks = Enumerable.Range(0, 25)
.Select(_ => Task.Run(() => Foo(() => SomeIOOperationAsync())));
await Task.WhenAll(tasks);
}
private static async Task<int> SomeIOOperationAsync()
{
await Task.Delay(100);
return 42;
}
}
В этом сценарии:
- Многие потоки пула потоков вызывают
Foo, который блокируется в.Result. - Каждая асинхронная операция завершает свои операции ввода-вывода и требует поток из пула потоков для выполнения обратного вызова завершения.
- Так как заблокированные вызовы занимают доступные рабочие потоки, завершение может ждать долгого времени, пока поток станет доступным.
- Современный .NET может увеличивать количество потоков в пуле с течением времени, но приложение по-прежнему может страдать от нехватки потоков в пуле, низкой пропускной способности, длительных задержек или видимого зависания.
Этот шаблон повлиял на HttpWebRequest.GetResponse в .NET Framework 1.x, где синхронный метод был реализован как оболочка вокруг асинхронного BeginGetResponse/EndGetResponse.
Руководство: Избегайте предоставления синхронных оболочек
Не предоставляйте синхронный метод, который упаковывает асинхронную реализацию. Вместо этого оставьте решение о том, следует ли блокировать потребителю. Потребитель знает свою среду потоков и может сделать обоснованный выбор.
Если вам нужно вызвать асинхронный метод синхронно, сначала рассмотрите, можно ли переструктурировать код, чтобы он был асинхронным на всех уровнях. Рефакторинг часто является лучшим долгосрочным решением.
Стратегии смягчения последствий при неизбежности использования синхронного кода поверх асинхронного
Иногда синхронизация через асинхронность действительно неизбежна. Например, это неизбежно при реализации интерфейса, требующего синхронного метода, и единственная доступная реализация является асинхронной. В этих случаях примените следующие стратегии для снижения риска.
Использование ConfigureAwait(false) в асинхронной реализации
Если вы управляете асинхронным методом, используйте Task.ConfigureAwait вместе с false для каждого await, чтобы предотвратить возврат продолжения к исходному SynchronizationContext.
public static class ConfigureAwaitMitigation
{
public static async Task<int> LibraryMethodAsync()
{
await Task.Delay(100).ConfigureAwait(false);
return 42;
}
public static int Sync()
{
return LibraryMethodAsync().GetAwaiter().GetResult();
}
}
Public Module ConfigureAwaitMitigation
Public Async Function LibraryMethodAsync() As Task(Of Integer)
Await Task.Delay(100).ConfigureAwait(False)
Return 42
End Function
Public Function Sync() As Integer
Return LibraryMethodAsync().Result
End Function
End Module
В качестве автора библиотеки используйте ConfigureAwait(false) все ожидания, если код не должен возобновиться в захваченном контексте. Использование ConfigureAwait(false) — это передовой метод для повышения производительности и помогает предотвратить взаимоблокировки, когда потребители блокируются.
Разгрузка в пул потоков
Если вы не управляете асинхронной реализацией (и она может не использовать ConfigureAwait(false)), передайте вызов в пул потоков. Пул потоков не имеет SynchronizationContext, поэтому ожидание не будет пытаться вернуться к заблокированному потоку.
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Тестирование в нескольких средах
Если необходимо отправить синхронную оболочку, проверьте ее из:
- Поток пользовательского интерфейса (WPF, Windows Forms).
- Пул потоков под нагрузкой.
- Пул потоков с низким максимальным числом потоков.
- Консольное приложение.
Поведение, которое работает в одной среде, может привести к взаимоблокировке в другой.