Synchronní obálky pro asynchronní metody

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:

  1. Vlákno uživatelského rozhraní volá Delay, které volá DelayAsync(milliseconds).Wait().
  2. DelayAsync běží synchronně, dokud nedosáhne await Task.Delay(milliseconds).
  3. Vzhledem k tomu, že zpoždění ještě není dokončené, await zachytí aktuální SynchronizationContext a pozastaví. DelayAsync vrátí volajícímu hodnotu Task .
  4. Vlákno uživatelského rozhraní se blokuje ve .Wait(), čeká na dokončení této úlohy.
  5. Po dokončení zpoždění musí pokračování běžet na původním SynchronizationContext vlákně uživatelského rozhraní.
  6. Vlákno uživatelského rozhraní nemůže zpracovat pokračování, protože je blokováno v .Wait().
  7. 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:

  1. Mnoho vláken ve fondu vláken volá Foo, který blokuje v .Result.
  2. 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í.
  3. 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.
  4. 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í.

Viz také