Szinkron burkolók aszinkron metódusokhoz

Ha egy kódtár csak aszinkron API-kat tesz elérhetővé, a felhasználók néha szinkron hívásokba csomagolják őket, hogy kielégítsék a szinkron felületet vagy szerződést. Ez a "sync-over-async" minta egyszerűnek tűnhet, de ez a holtpontok és a teljesítményproblémák gyakori forrása.

Alapszintű körbefuttatási minták

A tevékenységalapú aszinkron minta (TAP) metódus körüli szinkron burkoló hozzáfér a tevékenység tulajdonságához Result , amely blokkolja a hívó szálat:

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

Ez a megközelítés egyszerűnek tűnik, de súlyos problémákat okozhat attól függően, hogy milyen környezetben fut.

Holtpontok egyszálas környezetben

A legveszélyesebb forgatókönyv akkor fordul elő, ha egy egyszálas SynchronizationContextszálból szinkronizált burkolót hív meg. Ez a forgatókönyv általában egy felhasználói felületi szál WPF, Windows Forms vagy MAUI-alkalmazásokban.

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

Lépésről lépésre az alábbiak történnek:

  1. A UI szál meghívja Delay, amely meghívja DelayAsync(milliseconds).Wait().
  2. DelayAsync szinkron módon fut, amíg el nem éri await Task.Delay(milliseconds).
  3. Mivel a késés még nem fejeződött be, rögzíti az aktuális await állapotot, SynchronizationContext és felfüggeszti azt. DelayAsync a hívónak ad vissza egy Task értéket.
  4. A UI szál blokkolódik, várva arra, hogy a feladat befejeződjön .Wait().
  5. Ha a késés befejeződik, a folytatásnak az eredetin SynchronizationContext kell futnia, amely a felhasználói felület szála.
  6. A felhasználói felületi szál nem tudja feldolgozni a folytatást, mert blokkolva van .Wait().
  7. Holtpont.

Fontos

A szinkronizálási aszinkron kód sikeressége vagy sikertelensége attól függ, hogy melyik környezetben fut. A konzolalkalmazásokban működő kód holtpontot jelenthet egy felhasználói felületen vagy ASP.NET (.NET-keretrendszerben). Ez a környezeti függőség egy alapvető oka annak, hogy miért kerüljük a szinkron burkolók kitettségét.

Szálkészlet kimerülése

A holtpontok nem korlátozódnak a felhasználói felületi szálakra. Ha egy aszinkron módszer a szálkészlettől függ a munka befejezéséhez, például egy végső feldolgozási lépés sorba állításával számos készletszál szinkron burkolókkal való blokkolása éheztetheti a készletet:

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;
    }
}

Ebben a forgatókönyvben:

  1. Sok szálkezelő készlet szál hívja a Foo, amelyek blokkolnak a .Result.
  2. Minden aszinkron művelet befejezi az I/O-t, és egy szálkészlet-szálra van szüksége a befejezési visszahívás futtatásához.
  3. Mivel a blokkolt hívások az elérhető munkaszálakat foglalják el, a befejezések hosszú ideig várhatnak, amíg elérhetővé válik egy szál.
  4. A modern .NET idővel több szálkészlet-szálat adhat hozzá, de az alkalmazás továbbra is súlyos szálkészlet-éhezést, gyenge átviteli sebességet, hosszú késéseket vagy látszólagos lefagyást szenvedhet.

Ez a minta HttpWebRequest.GetResponse érintett az .NET Framework 1.x-ben, ahol a szinkron metódus burkolóként lett implementálva az aszinkron BeginGetResponse/EndGetResponse köré.

Útmutató: Kerülje a szinkron burkolók felfedését

Ne tegye közzé az aszinkron implementációt burkoló szinkron metódust. Ehelyett hagyja a fogyasztóra a döntést, hogy letiltsa-e vagy sem. A fogyasztó ismeri a szálkezelés környezetét, és megalapozott döntést hozhat.

Ha úgy találja, hogy szinkron módon kell meghívnia egy aszinkron metódust, először fontolja meg, hogy át tudja-e strukturálni a kódot úgy, hogy "aszinkron legyen teljesen lefelé". Az újrabontás gyakran a jobb hosszú távú megoldás.

Kockázatcsökkentési stratégiák, ha a szinkronizálás aszinkron módon nem elkerülhetetlen

Néha az aszinkron szinkronizálás valóban elkerülhetetlen. Ez például elkerülhetetlen, ha olyan felületet implementál, amely szinkron metódust igényel, és az egyetlen elérhető implementáció az aszinkron. Ezekben az esetekben alkalmazza az alábbi stratégiákat a kockázat csökkentésére.

Használja ConfigureAwait(false) az aszinkron implementációban

Ha ön vezérli az aszinkron metódust, használja Task.ConfigureAwait minden await esetén false, hogy megakadályozza a folytatás visszairányítását az eredeti SynchronizationContext-re.

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

Könyvtár készítőjeként használd a ConfigureAwait(false) minden várakozásnál, kivéve, ha a kódnak kifejezetten a rögzített környezetben kell folytatódnia. A ConfigureAwait(false) használata ajánlott eljárás a teljesítmény javítása érdekében, és segít megelőzni a holtpontokat, amikor a fogyasztók blokkolódnak.

Kiszervezés a szálkészletbe

Ha nem szabályozza az aszinkron implementációt (és lehet, hogy nem használja ConfigureAwait(false)), továbbítsa a hívást a szálkészletnek. A szálkészlet nem rendelkezik SynchronizationContext-val, ezért a várakozás nem próbálja meg visszairányítani a blokkolt szálat.

public int Sync()
{
    return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
    Return Task.Run(Function() Library.FooAsync()).Result
End Function

Tesztelés több környezetben

Ha szinkron burkolót kell szállítania, tesztelje ilyen környezetben:

  • Felhasználói felületi szál (WPF, Windows Forms).
  • A szálkészlet terhelés alatt.
  • Szálkészlet alacsony maximális szálszámmal.
  • Egy konzolalkalmazás.

Az egyik környezetben működő viselkedés holtpontot jelenthet egy másikban.

Lásd még