Megosztás a következőn keresztül:


A feladatalapú aszinkron minta implementálása

A feladatalapú aszinkron mintát (TAP) három módon implementálhatja: a C# és a Visual Basic fordítóinak használatával a Visual Studióban 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-keretrendszer 4.5-től kezdődően a kulcsszóval asyncAsync (a Visual Basicben) társított metódusok aszinkron metódusnak minősülnek, a C# és a Visual Basic fordítói pedig 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.

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

A TAP-mintát manuálisan is implementálhatja az implementáció 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:

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

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 például a hibrid megközelítést használni, ha egy fordító által létrehozott aszinkron metóduson kívül szeretné ellenőrizni az argumentumokat, hogy a kivételek ne az System.Threading.Tasks.Task objektumon keresztül, hanem a metódus közvetlen hívójához legyenek továbbítva.

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

private async Task<int> MethodAsyncInternal(string input)
{

   // code that uses await goes here

   return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
    If input Is Nothing Then Throw New ArgumentNullException("input")

    Return MethodAsyncInternal(input)
End Function

Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)

    ' code that uses await goes here

    return value
End Function

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ésű aszinkron műveleteket TAP-metódusokként is implementálhatja. Ha azonban a TAP-metódusok nyilvánosan elérhetővé válnak egy tárból, csak az I/O-hez kötött műveleteket tartalmazó számítási feladatokhoz kell megadni őket (ezek számítást is magukban foglalhatnak, de nem lehetnek tisztán számítási jellegűek). Ha egy metódus tisztán számítási kötöttségű, akkor csak szinkron implementációként kell elérhetővé tenni. Az azt használó kód ezután döntheti el, hogy a szinkron metódus meghívását egy feladatba csomagolja-e, hogy a munkát egy másik szálra kicsomagolja, vagy párhuzamosságot érjen el. Ha egy metódus I/O-kötésű, akkor csak aszinkron implementációként kell elérhetővé tenni.

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

Az System.Threading.Tasks.Task osztály ideálisan alkalmas számítási igényű műveletek ábrázolására. 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, és jelentős ellenőrzést biztosít az aszinkron számítások végrehajtásakor, hol és hogyan.

A számításhoz kötött tevékenységeket a következő módokon hozhatja létre:

  • 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. Egyszerűen elindíthat egy szálköteg által megcélzott számítási feladatot a Run használatával. Ez 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-keretrendszer 4-ben használja azt a TaskFactory.StartNew metódust, amely elfogad egy delegáltat (általában egy Action<T> vagy a 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 metódus overloadjai elfogadnak egy lemondási tokent (StartNew), feladatlétrehozási lehetőségeket (CancellationToken), valamint egy feladatütemezőt (TaskCreationOptions), amelyek mindegyike részletes vezérlést biztosít a feladat ütemezése és végrehajtása felett. Az aktuális feladatütemezőt megcélzó gyári példány elérhető az Factory osztály statikus tulajdonságaként (Task); 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 olyan tevékenységeket szabad visszaadni, amelyek már el lettek indítva.

  • 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 megadhatja a korábban említett módszerek egyikének, mint például a StartNew vagy Run módszernek, 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 idő előtt kiléphet, ha egy törlési kérelem érkezik a renderelés során. Ezenkívül, ha a lemondási kérelem a renderelés megkezdése előtt érkezik, meg kell akadályoznia a renderelési műveletet:

internal 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

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.

  • Egy OperationCanceledException kivétel kezeletlen marad egy ilyen feladat törzsén belül; ez a kivétel ugyanazt CancellationToken tartalmazza, amelyet a feladatnak átadnak, és ez a CancellationToken jelzi, hogy törlés kérése történt.

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 végződik, és 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

Olyan feladat létrehozásához, amelynek végrehajtását nem szabad folyamatosan egy szálhoz kötni, használja a TaskCompletionSource<TResult> típust. Ez a típus egy Task tulajdonságot tartalmaz, amely egy társított Task<TResult> példányt ad vissza. A feladat életciklusát olyan módszerek vezérlik TaskCompletionSource<TResult> , 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, hogy aszinkron módon meghívjon egy delegáltat egy meghatározott időtartam után, és a TaskCompletionSource<TResult> segítségével létrehozhat egy interfészt az időzítőhöz, 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 metódus erre a célra van megadva, és egy másik aszinkron metóduson belül 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álhat egy forrást dummy TResult-val (Boolean egy jó alapértelmezett választás, de ha aggódik amiatt, hogy a felhasználó a Task-t lecímkézi egy Task<TResult> típusúra, akkor használhat magánjellegű TResult típust helyette). 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> Delay(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 Delay(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, hanem a kettő keverékét is jelenthetik. Valójában több aszinkron művelet gyakran nagyobb vegyes műveletekbe van kombinálva. Az előző példában szereplő metódus például RenderAsync számításigényes műveletet hajtott 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 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

Ez a példa azt is bemutatja, hogyan lehet egyetlen lemondási tokent több aszinkron műveleten á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