Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Gdy biblioteka uwidacznia tylko asynchroniczne interfejsy API, konsumenci czasami opakowują je w synchroniczne wywołania, aby spełnić wymagania synchronicznego interfejsu lub kontraktu. Ten wzorzec zwany "sync-over-async" może wydawać się prosty, ale jest to typowe źródło zakleszczeń i problemów z wydajnością.
Podstawowe wzorce zawijania
Synchroniczna obudowa wokół metody Asynchronicznego Wzorca opartego na zadaniach (TAP) uzyskuje dostęp do właściwości zadania Result, która blokuje wątek wywołujący:
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
Takie podejście wygląda prosto, ale może powodować poważne problemy w zależności od środowiska, w którym działa.
Zakleszczenia w kontekstach jednowątkowych
Najbardziej niebezpiecznym scenariuszem jest wywołanie synchronicznej otoki z wątku, który ma pojedynczy wątek SynchronizationContext. Ten scenariusz jest zazwyczaj wątkiem interfejsu użytkownika w aplikacjach WPF, Windows Forms lub 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
Oto, co dzieje się krok po kroku:
- Wątek interfejsu użytkownika wywołuje metodę
Delay, która wywołuje metodęDelayAsync(milliseconds).Wait(). -
DelayAsyncdziała synchronicznie, dopóki nie osiągnieawait Task.Delay(milliseconds). - Ponieważ opóźnienie nie zostało jeszcze ukończone,
awaitprzechwytuje bieżący SynchronizationContext i zawiesza się.DelayAsynczwraca Task do wywołującego. - Wątek interfejsu użytkownika blokuje się w
.Wait(), czekając na ukończenie tego zadania. - Po zakończeniu opóźnienia kontynuacja musi zostać uruchomiona w oryginalnym
SynchronizationContextwątku interfejsu użytkownika. - Wątek interfejsu użytkownika nie może przetworzyć kontynuacji, ponieważ jest zablokowany w
.Wait(). - Zakleszczenie.
Ważna
Powodzenie lub niepowodzenie kodu synchronizacji nad asynchronicznym zależy od środowiska, w którym jest uruchamiany. Kod, który działa w aplikacji konsolowej, może spowodować zakleszczenie w wątku interfejsu użytkownika lub w aplikacji ASP.NET (na platformie .NET Framework). Ta zależność środowiskowa jest główną przyczyną, aby unikać eksponowania synchronicznych wrapperów.
Wyczerpanie puli wątków
Zakleszczenia nie ograniczają się tylko do wątków interfejsu użytkownika. Jeśli metoda asynchroniczna polega na puli wątków, aby ukończyć swoją pracę, na przykład kolejkowaniem końcowego kroku przetwarzania, blokowanie wielu wątków z użyciem synchronicznych obudów może zagłodzić pulę:
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;
}
}
W tym scenariuszu:
- Wiele wątków w puli wątków wywołuje
Foo, który blokuje w.Result. - Każda operacja asynchroniczna kończy operację we/wy i potrzebuje wątku z puli wątków, aby uruchomić jego funkcję zwrotną po zakończeniu.
- Ponieważ zablokowane wywołania zajmują dostępne wątki robocze, zakończenia mogą czekać przez dłuższy czas na udostępnienie wątku.
- Nowoczesny system .NET może z czasem dodawać więcej wątków do puli, ale aplikacja nadal może cierpieć na poważne niedobory puli wątków, niską wydajność, długie opóźnienia lub pozorne zawieszenie się.
Ten wzorzec wpłynął na HttpWebRequest.GetResponse w .NET Framework 1.x, gdzie metoda synchroniczna została zaimplementowana jako nakładka wokół asynchronicznego BeginGetResponse/EndGetResponse.
Wytyczne: Unikaj ujawniania synchronicznych opakowań
Nie udostępniaj metody synchronicznej, która opakowuje implementację asynchroniczną. Zamiast tego pozostaw decyzję, czy zablokować konsumentowi. Konsument, znając swoje środowisko wątkowe, może dokonać świadomego wyboru.
Jeśli okaże się, że trzeba wywołać metodę asynchroniczną synchronicznie, rozważ najpierw, czy możesz zrestrukturyzować kod jako "asynchroniczny aż w dół". Refaktoryzacja jest często lepszym rozwiązaniem długoterminowym.
Strategie łagodzenia, gdy synchronizacja z użyciem mechanizmów asynchronicznych jest nieunikniona
Czasami synchronizacja za pośrednictwem async jest naprawdę nieunikniona. Na przykład jest to nieuniknione podczas implementowania interfejsu wymagającego metody synchronicznej, a jedyną dostępną implementacją jest asynchroniczna. W takich przypadkach zastosuj następujące strategie, aby zmniejszyć ryzyko.
Użyj ConfigureAwait(false) w implementacji asynchronicznej
Jeśli kontrolujesz metodę asynchroniczną, użyj Task.ConfigureAwait z false na każdym await, aby uniemożliwić kontynuację przekazywania do oryginalnego 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
Jako autor biblioteki, należy używać ConfigureAwait(false) we wszystkich użyciach 'await', chyba że kod musi zostać wznowiony w przechwyconym kontekście. Użycie ConfigureAwait(false) jest najlepszą praktyką w zakresie wydajności i pomaga unikać deadlocków, gdy konsumenci się blokują.
Przekazywanie zadania do puli wątków
Jeśli nie kontrolujesz asynchronicznej implementacji (która może nie używać ConfigureAwait(false)), przenieś wywołanie do puli wątków. Pula wątków nie ma elementu SynchronizationContext, więc oczekiwanie nie spróbuje przeprowadzić marshalingu z powrotem do zablokowanego wątku:
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Testowanie w wielu środowiskach
Jeśli musisz dostarczyć synchroniczne opakowanie, przetestuj je z:
- Wątek interfejsu użytkownika (WPF, Windows Forms).
- Pula wątków jest obciążona.
- Pula wątków z małą maksymalną liczbą wątków.
- Aplikacja konsolowa.
Zachowanie, które działa w jednym środowisku, może utknąć w innym.