A feladatalapú aszinkron minta implementálása

A feladatalapú aszinkron mintát (TAP) három módon implementálhatja: a C# és Visual Basic fordítók használatával Visual Studio, manuálisan vagy a fordító és a manuális metódusok kombinációjával. Az alábbi szakaszok részletesen ismertetik az egyes metódusokat. A TAP-mintával számítási és I/O-kötésű aszinkron műveleteket is implementálhat. A Számítási feladatok szakasz az egyes műveletek típusait ismerteti.

TAP-metódusok létrehozása

A fordítók használata

A .NET Framework 4.5-től kezdve a async kulcsszóval (Async Visual Basic) társított metódusok aszinkron metódusnak minősülnek. A C# és Visual Basic fordítók végrehajtják a szükséges átalakításokat a metódus aszinkron implementálásához a TAP használatával. Az aszinkron metódusnak vagy System.Threading.Tasks.Task, vagy System.Threading.Tasks.Task<TResult> objektumot kell visszaadnia. Az utóbbi esetében a függvény törzsének egy értéket kell visszaadnia TResult, és a fordító biztosítja, hogy ez az eredmény elérhető legyen az eredményként kapott tevékenységobjektumon keresztül. Hasonlóképpen, a metódus törzsén belül kezeletlen kivételek a kimeneti feladathoz kerülnek továbbításra, és az eredményül kapott feladat a TaskStatus.Faulted állapotban ér véget. Ez alól a szabály alól kivételt képez, ha egy OperationCanceledException (vagy származtatott) típus kezeletlen lesz, ebben az esetben az eredményül kapott tevékenység az TaskStatus.Canceled állapotban fejeződik be.

Feladatindítás és feladatkezelés

Használjon Start csak olyan feladatokhoz, amelyeket kifejezetten olyan Task konstruktorokkal hoztak létre, amelyek még mindig Created állapotban vannak. A nyilvános TAP metódusoknak aktív feladatokat kell visszaadniuk, így a hívóknak nem szükséges hívást kezdeményezniük Start.

A legtöbb TAP-kódban ne szüntesse meg a feladatokat. Az A Task általában nem rendelkezik nem felügyelt erőforrásokkal, és az összes tevékenység eltávolítása többletterhelést okoz gyakorlati haszon nélkül. Csak akkor dobja el, amikor az adott API-k vagy mérések szükség esetét mutatják.

Ha olyan háttérfeladatot indít el, amely túlmutat a közvetlen hívási útvonalon, tartsa meg az egyértelmű tulajdonjogot, és kövesse a befejezést. További útmutatásért tekintse meg az aszinkron metódusok életben tartását ismertető témakört.

TAP-metódusok manuális létrehozása

Előfordulhat, hogy manuálisan implementálja a TAP-mintát a megvalósítás jobb szabályozása érdekében. A fordító a System.Threading.Tasks névtérből látható nyilvános felületre és a System.Runtime.CompilerServices névtérben található támogató típusokra támaszkodik. A TAP saját megvalósításához hozzon létre egy TaskCompletionSource<TResult> objektumot, hajtsa végre az aszinkron műveletet, és amikor befejeződik, hívja meg az SetResult, SetExceptionvagy SetCanceled metódust vagy az Try egyik metódus verzióját. Ha manuálisan implementál egy TAP-metódust, az eredményül kapott feladatot az aszinkron művelet befejeződésekor kell elvégeznie. Például:

static class StreamExtensions
{
    public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object? state)
    {
        var tcs = new TaskCompletionSource<int>();
        stream.BeginRead(buffer, offset, count, ar =>
        {
            try { tcs.SetResult(stream.EndRead(ar)); }
            catch (Exception exc) { tcs.SetException(exc); }
        }, state);
        return tcs.Task;
    }
}
Module StreamExtensions
    <Extension()>
    Public Function ReadTask(stream As Stream, buffer As Byte(),
                             offset As Integer, count As Integer,
                             state As Object) As Task(Of Integer)
        Dim tcs As New TaskCompletionSource(Of Integer)()
        stream.BeginRead(buffer, offset, count,
            Sub(ar)
                Try
                    tcs.SetResult(stream.EndRead(ar))
                Catch exc As Exception
                    tcs.SetException(exc)
                End Try
            End Sub, state)
        Return tcs.Task
    End Function
End Module

Hibrid megközelítés

Hasznos lehet a TAP-minta manuális implementálása, de az implementáció alapvető logikájának delegálása a fordítónak. Érdemes lehet a hibrid megközelítést alkalmazni például akkor, amikor egy fordító által generált aszinkron metóduson kívüli argumentumokat szeretne ellenőrizni, hogy a kivételek a metódus közvetlen hívójához jussanak el, ne pedig az System.Threading.Tasks.Task objektumon keresztül kerüljenek exponálásra.

class Calculator
{
    private int value = 0;

    public Task<int> MethodAsync(string input)
    {
        if (input == null) throw new ArgumentNullException(nameof(input));
        return MethodAsyncInternal(input);
    }

    private async Task<int> MethodAsyncInternal(string input)
    {
        // code that uses await goes here
        await Task.Delay(1);
        return value;
    }
}
Class Calculator
    Private value As Integer = 0

    Public Function MethodAsync(input As String) As Task(Of Integer)
        If input Is Nothing Then Throw New ArgumentNullException(NameOf(input))
        Return MethodAsyncInternal(input)
    End Function

    Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
        ' code that uses await goes here
        Await Task.Delay(1)
        Return value
    End Function
End Class

Egy másik eset, amikor az ilyen delegálás hasznos, az, amikor gyors elérési út optimalizálást hajt végre, és gyorsítótárazott feladatot szeretne visszaadni.

Munkaterhek

A számítási és az I/O-kötött aszinkron műveleteket TAP-metódusokként is implementálhatja. Ha azonban nyilvánosan teszi elérhetővé a TAP-metódusokat egy tárból, csak az I/O-hez kötött műveleteket tartalmazó számítási feladatokhoz adja meg őket. Ezek a műveletek számítást is magukban foglalhatnak, de nem szabad tisztán számítást végezniük. Ha egy metódus tisztán számításhoz kötött, csak szinkron implementációként tegye elérhetővé. Az azt használó kód ezután eldöntheti, hogy a szinkron metódus meghívását egy feladatba csomagolja-e a munka másik szálra való kiszervezéséhez vagy a párhuzamosság eléréséhez. Ha egy metódus I/O-kötött, csak aszinkron implementációként tegye közzé.

Számításhoz kötött tevékenységek

Az System.Threading.Tasks.Task osztály jól működik a számításigényes műveletek ábrázolása érdekében. Alapértelmezés szerint kihasználja az osztályon belüli speciális támogatást a ThreadPool hatékony végrehajtás érdekében. Emellett jelentős ellenőrzést biztosít az aszinkron számítások végrehajtásának időpontjára, hol és módjára.

Számítási feladatok létrehozása a következő módokon:

  • A .NET-keretrendszer 4.5-ös és újabb verzióiban (beleértve a .NET Core-t és a .NET 5+-ot) a statikus Task.Run metódus TaskFactory.StartNew-ként való használata egyszerűsítésként javasolt. Segítségével Run egyszerűen elindíthat egy számítási kötött feladatot, amely a szálkészletet célozza. Ez a módszer a számítási feladatok indításának előnyben részesített mechanizmusa. Közvetlenül csak akkor használja StartNew , ha részletesebben szeretné szabályozni a feladatot.

  • A .NET Framework 4-ben használja a TaskFactory.StartNew metódust. Elfogad egy meghatalmazottat (általában egy Action<T> vagy egy Func<TResult>) az aszinkron végrehajtáshoz. Ha delegáltat Action<T> ad meg, a metódus egy System.Threading.Tasks.Task olyan objektumot ad vissza, amely a meghatalmazott aszinkron végrehajtását jelöli. Ha meghatalmazottat Func<TResult> ad meg, a metódus egy objektumot System.Threading.Tasks.Task<TResult> ad vissza. A StartNew metódus túlterhelései elfogadnak egy törlési tokent (CancellationToken), feladatlétrehozási lehetőségeket (TaskCreationOptions), és egy feladatütemezőt (TaskScheduler). Ezek a paraméterek részletes vezérlést biztosítanak a tevékenység ütemezése és végrehajtása felett. Az aktuális feladatütemezőt megcélzó gyári példány az osztály statikus tulajdonságaként Factory (Task) érhető el. Például: Task.Factory.StartNew(…).

  • Ha külön szeretné létrehozni és ütemezni a feladatot, használja a Task típus és a Start metódus konstruktorait. A nyilvános metódusoknak csak a már megkezdett feladatokat szabad visszaadni.

  • Használja a metódus túlterhelt változatait Task.ContinueWith. Ez a metódus létrehoz egy új tevékenységet, amely egy másik tevékenység befejezésekor lesz ütemezve. Egyes túlterhelések lemondási ContinueWith jogkivonatot, folytatási lehetőségeket és feladatütemezőt fogadnak el a folytatási tevékenység ütemezésének és végrehajtásának jobb szabályozásához.

  • Használja a TaskFactory.ContinueWhenAll és TaskFactory.ContinueWhenAny módszereket. Ezek a metódusok létrehoznak egy új feladatot, amely akkor lesz ütemezve, amikor egy megadott tevékenységkészlet vagy bármelyik befejeződött. Ezek a módszerek túlterhelést is biztosítanak ezen tevékenységek ütemezésének és végrehajtásának szabályozásához.

A számítási feladatokban a rendszer megakadályozhatja az ütemezett tevékenységek végrehajtását, ha lemondási kérelmet kap, mielőtt elkezdené futtatni a feladatot. Ilyen esetben, ha lemondási jogkivonatot (CancellationToken objektumot) ad meg, a jogkivonatot átadhatja a jogkivonatot monitorozó aszinkron kódnak. A jogkivonatot a korábban említett módszerek egyikének is megadhatja, például StartNewRun úgy, hogy a Task futtatókörnyezet is figyelhesse a jogkivonatot.

Vegyünk példának egy aszinkron metódust, ami képet renderel. A feladat törzse lekérdezheti a törlési tokent, így a kód korán kiléphet, ha egy törlési kérés érkezik a renderelés során. Ezenkívül, ha a lemondási kérelem a renderelés megkezdése előtt érkezik meg, meg szeretné akadályozni a renderelési műveletet:

internal static Task<Bitmap> RenderAsync(ImageData data, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        var bmp = new Bitmap(data.Width, data.Height);
        for (int y = 0; y < data.Height; y++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            for (int x = 0; x < data.Width; x++)
            {
                // render pixel [x,y] into bmp
            }
        }
        return bmp;
    }, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As CancellationToken) As Task(Of Bitmap)
    Return Task.Run(Function()
                        Dim bmp As New Bitmap(data.Width, data.Height)
                        For y As Integer = 0 To data.Height - 1
                            cancellationToken.ThrowIfCancellationRequested()
                            For x As Integer = 0 To data.Width - 1
                                ' render pixel [x,y] into bmp
                            Next
                        Next
                        Return bmp
                    End Function, cancellationToken)
End Function

Megjegyzés:

Ez a minta Bitmap használ, amelyhez a System.Drawing.Common csomag szükséges, és csak Windows támogatja. A számításhoz kötött tevékenységminta – Task.RunCancellationToken használatával – minden platformra érvényes; nem Windows célokra helyettesítsen platformfüggetlen képalkotó könyvtárat.

A számításigényes feladatok Canceled állapotban fejeződnek be, ha az alábbi feltételek közül legalább az egyik teljesül:

  • A lemondási kérelem az CancellationToken objektumon keresztül érkezik, amely argumentumként szolgál a létrehozási módszerhez (például StartNewRun) mielőtt a tevékenység áttér az Running állapotra.

  • A OperationCanceledException kivétel nem lesz kezelve az ilyen feladat törzsén belül. Ez a kivétel ugyanazt CancellationToken tartalmazza, amelyet a rendszer átad a feladatnak, és ez a jogkivonat azt mutatja, hogy a rendszer lemondást kér.

Ha egy másik kivétel nem lesz kezelve a tevékenység törzsén belül, a tevékenység az Faulted állapotban fejeződik be. A tevékenységre való várakozásra vagy az eredmény elérésére tett kísérletek kivételt okoznak.

I/O-kötött tevékenységek

Ha olyan feladatot szeretne létrehozni, amely nem használ közvetlenül szálat a teljes végrehajtáshoz, használja a típust TaskCompletionSource<TResult> . Ez a típus egy Task tulajdonságot tartalmaz, amely egy társított Task<TResult> példányt ad vissza. A feladat TaskCompletionSource<TResult> életciklusát olyan módszerekkel szabályozhatja, mint SetResulta , SetException, SetCanceledés azok TrySet változatai.

Tegyük fel, hogy olyan feladatot szeretne létrehozni, amely egy megadott idő elteltével fejeződik be. Előfordulhat például, hogy késleltetni szeretne egy tevékenységet a felhasználói felületen. Az System.Threading.Timer osztály már lehetővé teszi a meghatalmazottak aszinkron meghívását egy megadott idő elteltével. A TaskCompletionSource<TResult> használatával Task<TResult> előlapot helyezhet az időzítőre. Például:

public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
    TaskCompletionSource<DateTimeOffset>? tcs = null;
    Timer? timer = null;

    timer = new Timer(delegate
    {
        timer!.Dispose();
        tcs!.TrySetResult(DateTimeOffset.UtcNow);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<DateTimeOffset>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
    Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(DateTimeOffset.UtcNow)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

A Task.Delay módszer erre a célra van megadva. Egy másik aszinkron metódusban is használhatja, például egy aszinkron lekérdezési ciklus implementálásához:

public static async Task Poll(Uri url, CancellationToken cancellationToken, IProgress<bool> progress)
{
    while (true)
    {
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
        bool success = false;
        try
        {
            await DownloadStringAsync(url);
            success = true;
        }
        catch { /* ignore errors */ }
        progress.Report(success);
    }
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
                           progress As IProgress(Of Boolean)) As Task
    Do While True
        Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
        Dim success As Boolean = False
        Try
            Await DownloadStringAsync(url)
            success = True
        Catch
            ' ignore errors
        End Try
        progress.Report(success)
    Loop
End Function

Az TaskCompletionSource<TResult> osztály nem rendelkezik nem általános megfelelővel. Azonban a Task<TResult> a Task-ből származik, így használhatja az általános TaskCompletionSource<TResult> objektumot olyan I/O-kötött metódusokhoz, amelyek egyszerűen csak egy feladatot adnak vissza. Ehhez használjon egy forrást dummy TResult-val (Boolean egy jó alapértelmezett választás, de ha aggódik amiatt, hogy a felhasználó Task típusát egy Task<TResult> típusra alakítja át, használhat helyette egy privát TResult típust). Az előző példában szereplő metódus például Delay az aktuális időt és az eredményül kapott eltolást (Task<DateTimeOffset>) adja vissza. Ha egy ilyen eredményérték szükségtelen, a metódus a következőképpen kódozható (figyelje meg a visszatérési típus módosítását és az argumentum TrySetResultmódosítását):

public static Task<bool> DelaySimple(int millisecondsTimeout)
{
    TaskCompletionSource<bool>? tcs = null;
    Timer? timer = null;

    timer = new Timer(delegate
    {
        timer!.Dispose();
        tcs!.TrySetResult(true);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<bool>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function DelaySimple(millisecondsTimeout As Integer) As Task(Of Boolean)
    Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(True)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of Boolean)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

Vegyes számítási és I/O-kötött tevékenységek

Az aszinkron metódusok nem csak számítási vagy I/O-kötött műveletekre korlátozódnak. Ezek a kettő keverékét jelölhetik. Valójában gyakran kombinál több aszinkron műveletet nagyobb vegyes műveletekbe. Az előző példában szereplő metódus például RenderAsync számításigényes műveletet hajt végre egy kép valamilyen bemeneten imageDataalapuló rendereléséhez. Ez imageData egy olyan webszolgáltatásból származhat, amelyhez aszinkron módon fér hozzá:

public static async Task<Bitmap> DownloadDataAndRenderImageAsync(CancellationToken cancellationToken)
{
    var imageData = await DownloadImageDataAsync(cancellationToken);
    return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(cancellationToken As CancellationToken) As Task(Of Bitmap)
    Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
    Return Await RenderAsync(imageData, cancellationToken)
End Function

Megjegyzés:

Ez a minta Bitmap használ, amelyhez a System.Drawing.Common csomag szükséges, és csak Windows támogatja. Az aszinkron letöltések aszinkron számításhoz kötött művelettel való láncolásának mintája minden platformra érvényes; nem Windows célokra helyettesítsen platformfüggetlen képalkotó könyvtárat.

Ez a példa azt is bemutatja, hogyan lehet egy törlési token-t több aszinkron műveleten keresztül átvezetni. További információkért tekintse meg a Feladat-alapú aszinkron minta alkalmazása című részben található lemondási útmutatót.

Lásd még