Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Když knihovna zveřejňuje pouze asynchronní rozhraní API, spotřebitelé je někdy zabalí do synchronních volání, která vyhovují synchronnímu rozhraní nebo kontraktu. Tento vzor "sync-over-async" se může zdát jednoduchý, ale je to běžný zdroj zablokování a problémů s výkonem.
Základní vzory obtékání
Synchronní obálka kolem metody asynchronního vzoru založeného na úlohách (TAP) přistupuje k vlastnosti úlohy Result, která blokuje volající vlákno.
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
Tento přístup vypadá jednoduše, ale může způsobit vážné problémy v závislosti na prostředí, ve kterém běží.
Zablokování při kontextech s jedním vláknem
K tomuto nebezpečnému scénáři dochází při volání synchronní obálky z vlákna, které má jednovláknové SynchronizationContext. Tento scénář je obvykle vlákno uživatelského rozhraní v aplikacích WPF (Windows Presentation Foundation), model Windows Forms nebo 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
Co se stane krok za krokem:
- Vlákno uživatelského rozhraní volá
Delay, které voláDelayAsync(milliseconds).Wait(). -
DelayAsyncběží synchronně, dokud nedosáhneawait Task.Delay(milliseconds). - Vzhledem k tomu, že zpoždění ještě není dokončené,
awaitzachytí aktuální SynchronizationContext a pozastaví.DelayAsyncvrátí volajícímu hodnotu Task . - Vlákno uživatelského rozhraní se blokuje ve
.Wait(), čeká na dokončení této úlohy. - Po dokončení zpoždění musí pokračování běžet na původním
SynchronizationContextvlákně uživatelského rozhraní. - Vlákno uživatelského rozhraní nemůže zpracovat pokračování, protože je blokováno v
.Wait(). - Zablokování.
Důležité
Úspěch či neúspěch synchronizace přes asynchronní kód závisí na prostředí, ve kterém běží. Kód, který funguje v konzolové aplikaci, může způsobit zablokování ve vlákně uživatelského rozhraní nebo v ASP.NET v rámci .NET Framework. Tato závislost na prostředí je základním důvodem, proč se vyhnout odhalení synchronních obalů.
Vyčerpání fondu vláken
Zablokování není omezeno na vlákna uživatelského rozhraní. Pokud asynchronní metoda závisí na fondu vláken pro dokončení své práce, například směrováním posledního kroku zpracování do fronty, blokování mnoha vláken fondu pomocí synchronních obálek může způsobit vyčerpání zdrojů fondu:
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;
}
}
V tomto scénáři:
- Mnoho vláken ve fondu vláken volá
Foo, který blokuje v.Result. - Každá asynchronní operace dokončí své vstupně-výstupní úlohy a potřebuje vlákno fondu ke spuštění dokončovacího zpětného volání.
- Vzhledem k tomu, že blokovaná volání zabírají dostupná pracovní vlákna, můžou dokončení čekat dlouhou dobu, než bude vlákno k dispozici.
- Moderní .NET může v průběhu času přidávat další vlákna fondu vláken, ale aplikace může stále trpět vyhladověním fondu vláken, nízkou propustností, dlouhým zpožděním nebo zdánlivým zablokováním.
Tento vzor ovlivnil HttpWebRequest.GetResponse v .NET Framework 1.x, kde synchronní metoda byla implementována jako obálka kolem asynchronního BeginGetResponse/EndGetResponse.
Pokyny: Vyhněte se zveřejnění synchronních obálk
Nezpřístupňujte synchronní metodu, která zabalí asynchronní implementaci. Místo toho nechte rozhodnutí, zda blokovat, na spotřebiteli. Spotřebitel zná své prostředí vláken a může učinit informovanou volbu.
Pokud zjistíte, že potřebujete volat asynchronní metodu synchronně, zvažte nejprve, jestli můžete restrukturalizovat kód tak, aby byl "asynchronní až dolů". Refaktoring je často lepším dlouhodobým řešením.
Strategie zmírnění v případech, kdy synchronizace přes async je nevyhnutelná
Někdy je synchronizace přes async skutečně neupravitelná. Například je nevyhnutelné, když implementujete rozhraní, které vyžaduje synchronní metodu, a jedinou dostupnou implementací je asynchronní. V těchto případech použijte následující strategie ke snížení rizika.
Použití ConfigureAwait(false) v asynchronní implementaci
Pokud řídíte asynchronní metodu, použijte Task.ConfigureAwait spolu s false na každé await, abyste zabránili vrácení pokračování do původního 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 knihovny používejte ConfigureAwait(false) na všechny operátory await, pokud váš kód výslovně nepotřebuje pokračovat v zachyceném kontextu. Použití ConfigureAwait(false) je osvědčeným postupem pro výkon a pomáhá zabránit zablokování, když spotřebitelé blokují.
Přesunout úlohy do fondu vláken
Pokud neřídíte asynchronní implementaci (a nemusí se použít ConfigureAwait(false)), předejte úlohu do fondu vláken. Fond vláken neobsahuje SynchronizationContext, takže await se nebude snažit vrátit do zablokovaného vlákna:
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Testování v několika prostředích
Pokud musíte odeslat synchronní obálku, otestujte ji z:
- Vlákno uživatelského rozhraní (WPF (Windows Presentation Foundation), model Windows Forms).
- Fond vláken pod zatížením.
- Pool vláken s nízkým maximálním počtem vláken.
- Konzolová aplikace.
Chování, které funguje v jednom prostředí, může dojít k vzájemnému zablokování.