Aszinkron burkolók szinkron metódusokhoz

Ha szinkron metódussal rendelkezik egy könyvtárban, előfordulhat, hogy szeretne egy aszinkron megfelelő verziót elérhetővé tenni, amely azt Task.Run-ba burkolja.

public T Foo() { /* synchronous work */ }

// Don't do this in a library:
public Task<T> FooAsync()
{
    return Task.Run(() => Foo());
}

Ez a cikk azt ismerteti, hogy ez a megközelítés miért szinte mindig helytelen a könyvtárak számára, és hogyan kell gondolni a kompromisszumokra.

Méretezhetőség vs. kiszervezés

Az aszinkron programozás két különböző előnnyel jár:

  • Méretezhetőség – Csökkentheti az erőforrás-felhasználást a szálak felszabadításával az I/O-várakozások során.
  • Munka áthelyezése szálra – A munka áthelyezése másik szálra a válaszkészség fenntartása érdekében (például a felhasználói felület szálának szabadon tartása) vagy a párhuzam elérése érdekében.

Ezek az előnyök különböző megközelítéseket igényelnek. A kritikus különbség: a szinkron metódus Task.Run burkolása segít a feladatátadásban, de nem tesz semmit a méretezhetőséghez.

Miért Task.Run nem javítja a méretezhetőséget?

A valóban aszinkron implementáció csökkenti a hosszú ideig futó művelet során felhasznált szálak számát. A Task.Run burkoló továbbra is blokkolja a szálakat – csak áthelyezi a blokkolást az egyik szálról a másikra:

public static class TimerExampleWrong
{
    public static Task SleepAsync(int millisecondsTimeout)
    {
        return Task.Run(() => Thread.Sleep(millisecondsTimeout));
    }
}
Public Module TimerExampleWrong
    Public Function SleepAsync(millisecondsTimeout As Integer) As Task
        Return Task.Run(Sub() Thread.Sleep(millisecondsTimeout))
    End Function
End Module

Hasonlítsa össze ezt a megközelítést egy valóban aszinkron implementációval, amely várakozás közben nem használ szálakat:

public static class TimerExampleRight
{
    public static Task SleepAsync(int millisecondsTimeout)
    {
        var tcs = new TaskCompletionSource<bool>();
        var timer = new Timer(
            _ => tcs.TrySetResult(true), null, millisecondsTimeout, Timeout.Infinite);

        tcs.Task.ContinueWith(
            _ => timer.Dispose(), TaskScheduler.Default);

        return tcs.Task;
    }
}
Public Module TimerExampleRight
    Public Function SleepAsync(millisecondsTimeout As Integer) As Task
        Dim tcs As New TaskCompletionSource(Of Boolean)()
        Dim tmr As New Timer(
            Sub(state) tcs.TrySetResult(True), Nothing, millisecondsTimeout, Timeout.Infinite)

        tcs.Task.ContinueWith(
            Sub(t) tmr.Dispose(), TaskScheduler.Default)

        Return tcs.Task
    End Function
End Module

Mindkét implementáció befejeződik a megadott késleltetés után, de a második implementáció nem blokkolja a szálat várakozás közben. A több egyidejű kérést kezelő kiszolgálóalkalmazások esetében ez a különbség közvetlenül befolyásolja, hogy egy kiszolgáló hány kérést tud egyszerre feldolgozni.

Az átterhelés a fogyasztó felelőssége

Szinkron hívások Task.Run keretben történő végrehajtása hasznos lehet a UI szálról való munka áthelyezésére. A fogyasztónak, és nem a kódtárnak, kell kezelnie ezt a csomagolást:

public static class UIOffloadExample
{
    public static int ComputeIntensive(int input)
    {
        int result = 0;
        for (int i = 0; i < input; i++)
        {
            result += i;
        }
        return result;
    }

    public static async Task ConsumeFromUIThreadAsync()
    {
        int result = await Task.Run(() => ComputeIntensive(10_000));
        Console.WriteLine($"Result: {result}");
    }
}
Public Module UIOffloadExample
    Public Function ComputeIntensive(input As Integer) As Integer
        Dim result As Integer = 0
        For i As Integer = 0 To input - 1
            result += i
        Next
        Return result
    End Function

    Public Async Function ConsumeFromUIThreadAsync() As Task
        Dim result As Integer = Await Task.Run(Function() ComputeIntensive(10_000))
        Console.WriteLine($"Result: {result}")
    End Function
End Module

A fogyasztó ismeri a környezetét: hogy UI szálon fut-e, mennyi részletességre van szüksége, és hogy a leterhelés hozzáadott értéket képvisel-e. A könyvtár nem működik.

Miért nem szabad a könyvtáraknak elérhetővé tenni az aszinkron-szinkron burkolókat?

Ha egy kódtár csak a szinkron metódust teszi elérhetővé (és nem aszinkron burkolót), a fogyasztók többféleképpen is profitálnak:

  • Csökkentett API-felület: Kevesebb tanulásra, tesztelésre és karbantartásra használható módszer.
  • Nincs félrevezető méretezhetőségi elvárás: A felhasználók tudják, hogy csak az aszinkronként közzétett módszerek biztosítják a méretezhetőség előnyeit.
  • Fogyasztói ellenőrzés: A hívók eldöntik, hogy kiszerveznek-e, és hogyan valósítják meg, a megfelelő részletességi szinten. A nagy átviteli sebességű kiszolgálóalkalmazások közvetlenül meghívhatják a szinkron metódust, így elkerülhető a felesleges többletterhelés.Task.Run
  • Jobb teljesítmény: Az aszinkron burkolók terhelést okoznak a foglalások, a környezeti kapcsolók és a szálkészlet ütemezése révén. A részletes műveletek esetében ez a többletterhelés jelentős lehet.

A szabály alóli kivételek

Egyes alaposztályok az aszinkron metódusokat teszik elérhetővé, hogy a származtatott osztályok valóban aszinkron implementációkkal felülbírálhassák őket. Az alaposztály alapértelmezett aszinkron szinkronizálást biztosít.

Például elérhetővé Stream teszi ReadAsync és WriteAsync. Az alap implementációk a szinkron Read és Write a metódusokat burkolják. A származtatott osztályok, mint a FileStream és NetworkStream, felülbírálják ezeket a metódusokat aszinkron I/O-implementációkkal, amelyek valós méretezhetőségi előnyöket biztosítanak.

Hasonlóképpen, TextReader burkolóként biztosítja a ReadToEndAsync az alaposztályon, és StreamReader felülbírálja azt egy valóban aszinkron implementációval, amely belülről hív egy ReadAsync-t.

Ezek a kivételek a következők miatt érvényesek:

  • A minta polimorfizmusra lett tervezve. A hívók az alaptípussal kommunikálnak.
  • A származtatott típusok valóban aszinkron felülbírálásokat biztosítanak.

Irányelv

Csak akkor tegye elérhetővé az aszinkron metódusokat egy könyvtárból, ha az implementáció valós méretezhetőségi előnyöket biztosít a szinkron megfelelőjéhez képest. Ne tegye közzé az aszinkron metódusokat kizárólag átterhelés céljából. Ezt a döntést a fogyasztóra bízza.

Lásd még