Feladatalapú aszinkron minta (TAP) a .NET-ben: Bevezetés és áttekintés

A .NET-ben a Task alapú aszinkron minta az új fejlesztéshez javasolt aszinkron tervezési minta. A System.Threading.Tasks névtérben lévő Task és Task<TResult> típusokon alapul, amelyek aszinkron műveleteket jelképeznek.

Elnevezés, paraméterek és visszatérési típusok

A TAP egyetlen módszert használ az aszinkron művelet indításának és befejezésének ábrázolására. Ez a megközelítés ellentétben áll az Aszinkron programozási modell (APM vagy IAsyncResult) mintával és az eseményalapú aszinkron mintával (EAP). Az APM megköveteli a Begin és a End metódusokat. Az EAP olyan metódust igényel, amelynek utótagja Async van, és egy vagy több eseményt, eseménykezelő delegálttípust és EventArg-származtatott típust is igényel. Az aszinkron metódusok a TAP-ban tartalmazzák a Async utótagot a művelet neve után az olyan metódusok esetében, amelyek várható típusokat adnak vissza, például Task, Task<TResult>, ValueTask és ValueTask<TResult>. Egy aszinkron Get művelet, amely egy Task<String> értéket ad vissza, elnevezhető GetAsync. Ha olyan osztályhoz ad hozzá TAP metódust, amely már tartalmaz egy EAP-metódusnevet az Async utótaggal, használja helyette az utótagot TaskAsync . Ha például az osztály már rendelkezik metódussal GetAsync , használja a nevet GetTaskAsync. Ha egy metódus aszinkron műveletet indít el, de nem ad vissza várandós típust, a metódus nevének a következővel Beginkell kezdődnie: , Startvagy valamilyen más ige, amely arra utal, hogy ez a metódus nem adja vissza vagy nem dobja el a művelet eredményét.

A TAP metódus vagy egy System.Threading.Tasks.Task, vagy egy System.Threading.Tasks.Task<TResult> értéket ad vissza, attól függően, hogy a megfelelő szinkron metódus void típussal tér vissza, vagy valamilyen TResult típust ad vissza.

A TAP-metódus paramétereinek egyeznie kell a szinkron megfelelője paramétereivel, és ugyanabban a sorrendben kell megadni. Azonban out és ref paraméterek mentesülnek a szabály alól, ezért teljesen el kell kerülni őket. bármely adatnak, amelyet egy out vagy ref paraméter ad vissza, a Task<TResult> által visszaadott TResult részét kell képeznie, és több érték elhelyezéséhez használjon egy tuple-t vagy egy egyéni adatstruktúrát. Érdemes lehet paramétert CancellationToken hozzáadni akkor is, ha a TAP-metódus szinkron megfelelője nem kínál egyet.

A kizárólag a tevékenységek létrehozására, kezelésére vagy kombinációjára fordított metódusoknak (ahol a metódus aszinkron szándéka egyértelműen szerepel a metódus nevében vagy annak a típusnak a nevében, amelyhez a metódus tartozik) nem kell követni ezt az elnevezési mintát. Ezeket a módszereket gyakran kombinátoroknak nevezik. Példák a kombinátorokra: WhenAll és WhenAny, amelyekről a Beépített feladatalapú kombinátorok használata című szakasz tárgyalja a Feladatalapú aszinkron minta fogyasztása című cikkben.

Ha például a TAP szintaxis eltér az örökölt aszinkron programozási mintákban, például az Aszinkron programozási modellben (APM) és az eseményalapú aszinkron mintában (EAP) használt szintaxistól, tekintse meg az aszinkron programozási mintákat.

Aszinkron viselkedés, visszatérési típusok és elnevezés

A async kulcsszó nem kényszeríti a metódust arra, hogy aszinkron módon fusson egy másik szálon. Engedélyezi await, és a metódus szinkron módon fut, amíg el nem ér egy befejezetlen várható objektumot. Ha a metódus nem éri el a várva várt befejezetlen értéket, szinkron módon is befejeződhet.

A legtöbb API esetében előnyben részesítse a következő visszatérési típusokat:

  • Olyan aszinkron műveletekhez használható Task , amelyek nem hoznak létre értéket.
  • Értékeket termelő aszinkron műveletekhez használható Task<TResult> .
  • Használjon ValueTask vagy ValueTask<TResult> csak akkor, ha a mérések kiosztási nyomást mutatnak, és amikor a fogyasztók képesek kezelni a többlethasználati korlátozásokat.

A TAP elnevezésének kiszámíthatónak tartása:

  • Használja az Async utótagot a várt típusokat visszaadó metódusokhoz.
  • Ne fűzze hozzá a Async-t a szinkron metódusokhoz.
  • Adja hozzá az új MethodNameAsync túlterhelést a meglévő "MethodName" mellé. Ne távolítsa el vagy nevezze át a szinkron API-t. Mindkettő megtartása lehetővé teszi, hogy a hívások saját tempóban, törés nélkül hajtsák végre a migrálást.

Aszinkron művelet kezdeményezése

A TAP-on alapuló aszinkron metódusok kis mennyiségű munkát végezhetnek szinkron módon, például az argumentumok érvényesítése és az aszinkron művelet kezdeményezése, mielőtt visszaadják az eredményül kapott feladatot. A szinkron munkát minimálisra kell csökkenteni, hogy az aszinkron metódus gyorsan visszatérjen. A gyors visszatérés okai a következők:

  • Előfordulhat, hogy aszinkron metódusokat hív meg a felhasználói felületi (UI) szálakból, és a hosszú ideig futó szinkron munka károsíthatja az alkalmazás válaszképességét.
  • Egyszerre több aszinkron metódust is elindíthat. Ezért az aszinkron módszer szinkron részében végzett minden hosszú ideig futó munka késleltetheti az egyéb aszinkron műveletek indítását, ezáltal csökkentve az egyidejűség előnyeit.

Bizonyos esetekben a művelet végrehajtásához szükséges munka mennyisége kisebb, mint a művelet aszinkron elindításához szükséges munka mennyisége. Ilyen forgatókönyv például az olyan adatfolyamból való olvasás, amelyben az olvasási művelet kielégíthető a memóriában már pufferelt adatokkal. Ilyen esetekben előfordulhat, hogy a művelet szinkronban fejeződik be, és egy már befejezett feladatot ad vissza.

Kivételek

Az aszinkron metódusnak kivételt kell kivennie közvetlenül az aszinkron metódushívásból, csak használati hibára válaszul. A használati hibák soha nem fordulhatnak elő az éles kódban. Ha például egy metódus egyik argumentumaként nullhivatkozást ad át (Nothing a Visual Basic-ben), és ez hibás állapotot okoz (amit általában egy ArgumentNullException kivétel jelez), módosíthatja a hívó kódot, hogy a nullhivatkozás soha ne legyen átadva. Minden egyéb hiba esetén rendeljen hozzá kivételeket, amelyek akkor fordulnak elő, ha egy aszinkron metódus fut a visszaadott tevékenységen, még akkor is, ha az aszinkron metódus szinkron módon fejeződik be a feladat visszaadása előtt. A tevékenységek általában legfeljebb egy kivételt tartalmaznak. Ha azonban a tevékenység több műveletet jelöl (például WhenAll), több kivétel is társítható egyetlen tevékenységhez.

Célkörnyezet

A TAP metódus implementálásakor meghatározhatja, hogy hol történik az aszinkron végrehajtás. Dönthet úgy, hogy végrehajtja a számítási feladatot a szálkészleten, aszinkron I/O használatával valósítja meg (anélkül, hogy a művelet végrehajtásához egy szálhoz lenne kötve), futtathatja egy adott szálon (például a felhasználói felületen), vagy bármilyen számú lehetséges környezetet használhat. Előfordulhat, hogy a TAP-metódusnak még nincs mit végrehajtania, és csak egy Task olyan feltételt ad vissza, amely a rendszer más részeinek előfordulását jelzi (például egy olyan feladatot, amely egy várólistára helyezett adatstruktúrába érkező adatokat jelöl).

A TAP metódus hívója letilthatja a TAP metódus befejezésére való várakozást az eredményül kapott tevékenységre való szinkron várakozással, vagy további (folytatási) kódot futtathat az aszinkron művelet befejezésekor. A folytatási kód létrehozója szabályozza a kód végrehajtásának helyét. A folytatási kódot explicit módon is létrehozhatja a Task osztály metódusaival (például ContinueWith), vagy implicit módon, a folytatásokra épülő nyelvi támogatással (például await C#-ban, Await Visual Basic, AwaitValue F#-ban).

Tevékenység állapota

Az Task osztály egy életciklust biztosít az aszinkron műveletekhez, és ezt a ciklust az TaskStatus enumerálás képviseli. Az Task és Task<TResult> típusokból származó szélsőséges esetek támogatása érdekében, valamint az építés ütemezéstől való elválasztás támogatására a Task osztály egy Start metódust tesz elérhetővé. A nyilvános Task konstruktorok által létrehozott tevékenységeket hideg tevékenységeknek nevezzük, mivel nem ütemezett Created állapotban kezdik meg az életciklusukat, és csak akkor ütemezettek, amikor Start eljárást meghívják ezeken a példányokon.

Minden más feladat az életciklusát aktív állapotban kezdi, ami azt jelenti, hogy az általuk képviselt aszinkron műveletek már elindultak, és a feladat állapota nem azonos a TaskStatus.Created felsorolt értékkel. A TAP metódusokból visszaadott összes feladatot aktiválni kell. Ha egy TAP-metódus belsőleg egy tevékenység konstruktorával hozza létre a visszaadni kívánt feladatot, a TAP metódusnak a visszaadása előtt fel kell hívnia Start az Task objektumot. A TAP metódus felhasználói nyugodtan feltételezhetik, hogy a visszaadott feladat aktív, és nem szabad megpróbálni meghívni Start a TAP metódusból visszaadott feladatokat Task . Egy aktív feladat Start meghívása InvalidOperationException kivételt eredményez.

A feladat aktiválása utáni „fire-and-forget” típusú élettartam és tulajdonjog kezelési útmutatóját az aszinkron metódusok életben tartása című témakörben találja.

Lemondás (nem kötelező)

A TAP-ban a lemondás nem kötelező mind az aszinkron metódus implementálói, mind az aszinkron metódusfelhasználók számára. Ha egy művelet engedélyezi a törlést, elérhetővé teszi az aszinkron metódus olyan túlterhelését, amely elfogad egy törlési tokent (CancellationToken példányt). Konvenció szerint a paraméter neve cancellationToken.

public static Task ReadAsync(byte[] buffer, int offset, int count,
                             CancellationToken cancellationToken)
Public Function ReadAsync(buffer As Byte(), offset As Integer, count As Integer,
                          cancellationToken As CancellationToken) As Task

Az aszinkron művelet figyeli ezt a tokent a lemondási kérelmek figyelésére. Ha lemondási kérelmet kap, dönthet úgy, hogy tiszteletben tartja a kérést, és megszakítja a műveletet. Ha a lemondási kérelem idő előtt befejezi a munkát, a TAP metódus egy feladatot ad vissza, amely a Canceled állapotban végződik; nincs elérhető eredmény, és nem dobható kivétel. Az Canceled állapotot egy tevékenység végleges (befejezett) állapotának tekintjük, az Faulted állapotokkal együtt RanToCompletion . Ezért ha egy tevékenység a Canceled állapotban van, a IsCompleted tulajdonsága true értéket ad vissza. Amikor egy feladat a Canceled állapotban befejeződik, a feladathoz regisztrált folytatásokat ütemezik vagy végrehajtják, kivéve, ha például a NotOnCanceled lehetőséget használták a folytatás letiltására. Minden olyan kód, amely aszinkron módon vár egy törölt feladatra a programozási nyelvi funkciók használatával, továbbra is fut, de kap egy OperationCanceledException-t vagy egy abból származó kivételt. A feladatra szinkron módon várakozó kód, például WaitWaitAll egy kivétellel továbbra is futtatható.

Ha a lemondási token a token meghívását elfogadó TAP metódus előtt kéri a lemondást, a TAP metódusnak egy Canceled feladatobjektumot kell visszaadnia. Ha azonban az aszinkron művelet futtatása közben lemondást kér, az aszinkron műveletnek nem kell elfogadnia a lemondási kérelmet. A visszaadott tevékenység csak akkor fejeződhet be az Canceled állapotban, ha a művelet a lemondási kérelem eredményeként fejeződik be. Amennyiben lemondást kérnek, de ennek ellenére egy eredmény vagy kivétel keletkezik, a feladatnak RanToCompletion vagy Faulted állapotban kell befejeződnie.

Azoknak az aszinkron metódusoknak, amelyek elsősorban a törlés lehetőségét szeretnék feltárni, nem kell olyan túlterhelést kínálniuk, ami nem fogad el törlési tokent. A nem megszakítható metódusok esetében ne adjon meg olyan túlterheléseket, amelyek elfogadják a lemondási tokent; ez segít jelezni a hívónak, hogy a célmetódus valóban megszakítható-e. Az olyan fogyasztói kód, amely nem kíván megszakítást, meghívhat egy metódust, amely elfogad egy CancellationToken-t és megadja a None-t argumentumként. None funkcionálisan megegyezik az alapértelmezett CancellationTokenértékével.

Állapotjelentés (nem kötelező)

Az aszinkron műveletek némelyike kihasználja az előrehaladási értesítéseket. Ezek az értesítések általában az aszinkron művelet előrehaladásával kapcsolatos információkkal frissítik a felhasználói felületet.

A TAP-ban kezelje az előrehaladást egy IProgress<T> felületen keresztül. Adja át ezt az interfészt az aszinkron metódusnak paraméterként, általában elnevezve progress. Ha az aszinkron metódus meghívásakor megadja a folyamatjelző felületet, segít kiküszöbölni a helytelen használatból eredő versenyfeltételeket. Ezek a versenyfeltételek akkor fordulnak elő, ha az eseménykezelők helytelenül vannak regisztrálva a művelet elindítása után, és elmulasztják a frissítéseket. Ennél is fontosabb, hogy a folyamatjelző felület támogatja a folyamat különböző implementációit, amelyeket a fogyasztó kód határoz meg. Előfordulhat például, hogy a felhasználó kód csak a legújabb állapotfrissítéssel foglalkozik, vagy szeretné pufferelni az összes frissítést, minden frissítéshez meghívni egy műveletet, vagy szabályozni, hogy a hívás egy adott szálra van-e rendezve. Mindezek a lehetőségek a felület különböző implementációinak használatával érhetők el, az adott fogyasztó igényeinek megfelelően. A lemondáshoz hasonlóan a TAP-implementációknak is csak akkor kell paramétert IProgress<T> megadniuk, ha az API támogatja az előrehaladási értesítéseket.

Ha például a ReadAsync cikkben korábban tárgyalt módszer az eddig beolvasott bájtok számának formájában jelentheti a köztes haladást, a folyamatvisszahívás lehet egy IProgress<T> interfész:

public static Task ReadAsync(byte[] buffer, int offset, int count,
                             IProgress<long> progress)
Public Function ReadAsync(buffer As Byte(), offset As Integer, count As Integer,
                          progress As IProgress(Of Long)) As Task

Ha egy FindFilesAsync metódus egy adott keresési mintának megfelelő összes fájl listáját adja vissza, az előrehaladási visszahívás becslést adhat a befejezett munka százalékos arányáról és a részleges eredmények aktuális készletéről. Az információt megadhatja egy tuple segítségével is:

public static Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<Tuple<double, ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(
    pattern As String,
    progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) As Task(Of ReadOnlyCollection(Of FileInfo))

vagy az API-ra jellemző adattípussal:

public static Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(
    pattern As String,
    progress As IProgress(Of FindFilesProgressInfo)) As Task(Of ReadOnlyCollection(Of FileInfo))

Az utóbbi esetben a speciális adattípust általában utótaggal ProgressInfoírjuk.

Ha a TAP-implementációk olyan túlterheléseket biztosítanak, amelyek elfogadják a paramétert progress , engedélyezniük kell az argumentumot null. Ha átmegy null, nem lesz jelentve előrehaladás. A TAP-implementációknak szinkron módon kell jelentenie az előrehaladást az Progress<T> objektumnak, ami lehetővé teszi, hogy az aszinkron metódus gyorsan biztosítsa az előrehaladást. Lehetővé teszi továbbá, hogy a folyamat fogyasztója határozza meg, hogyan és hol érdemes a legjobban kezelni az információkat. A folyamatpéldány például dönthet úgy, hogy végrehajtja a visszahívásokat, és eseményeket hoz létre egy rögzített szinkronizálási környezetben.

IProgress<T-implementációk>

A .NET biztosítja az Progress<T> osztályt, amely implementálja a IProgress<T>. A Progress<T> osztály a következőképpen van deklarálva:

public class Progress<T> : IProgress<T>
{
    public Progress();
    public Progress(Action<T> handler);
    protected virtual void OnReport(T value);
    public event EventHandler<T>? ProgressChanged;
}

Az Progress<T> példány kitesz egy ProgressChanged eseményt, amely minden alkalommal kiváltódik, amikor az aszinkron művelet jelentést tesz egy állapotfrissítésről. Az ProgressChanged esemény azon az SynchronizationContext objektumon jön létre, amelyet a Progress<T> példány annak példányosításakor rögzít. Ha nem érhető el szinkronizálási környezet, a rendszer egy alapértelmezett környezetet használ, amely a szálkészletet célozza. Ezzel az eseménysel regisztrálhat kezelőket. A kényelem érdekében egyetlen kezelőt is megadhat a Progress<T> konstruktornak. Ez a kezelő ugyanúgy viselkedik, mint az esemény eseménykezelője ProgressChanged . A folyamatfrissítések aszinkron módon jelennek meg, így elkerülhető az aszinkron művelet késleltetése az eseménykezelők végrehajtása közben. Egy másik IProgress<T> implementáció dönthet úgy, hogy különböző szemantikát alkalmaz.

A biztosítandó túlterhelések kiválasztása

Ha a TAP-implementáció az opcionális CancellationToken és az opcionális IProgress<T> paramétereket is használja, az akár négy túlterhelést is igényelhet:

public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);
Public MethodNameAsync(…) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task

Számos TAP-implementáció azonban nem biztosít lemondási vagy előrehaladási képességeket, ezért egyetlen módszert igényelnek:

public Task MethodNameAsync(…);
Public MethodNameAsync(…) As Task

Ha a TAP-implementáció támogatja a lemondást vagy az előrehaladást, de mindkettőt nem, az két túlterhelést eredményezhet:

public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);

// … or …

public Task MethodNameAsync(…);
public Task MethodNameAsync(…, IProgress<T> progress);
Public MethodNameAsync(…) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task

' … or …

Public MethodNameAsync(…) As Task
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task

Ha a TAP-implementáció támogatja a lemondást és az előrehaladást is, az mind a négy túlterhelést elérhetővé teheti. Ez azonban csak a következő kettőt biztosíthatja:

public Task MethodNameAsync(…);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);
Public MethodNameAsync(…) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task

A két hiányzó köztes kombináció kompenzálásához a fejlesztők None-t vagy alapértelmezett CancellationToken-t adhatnak meg a cancellationToken paraméterhez, és null-t a progress paraméterhez.

Ha azt várja, hogy a TAP metódus minden használata támogassa a megszakítást vagy a haladást, elhagyhatja azokat a túlterheléseket, amelyek nem fogadják el a megfelelő paramétert.

Ha úgy dönt, hogy több túlterhelést is elérhetővé tesz annak érdekében, hogy a törlés vagy az előrehaladás opcionálissá váljon, a törlést vagy előrehaladást nem támogató túlterheléseknek úgy kell működniük, mintha a törléshez None vagy az előrehaladáshoz null paramétereket adták volna át annak a túlterhelésnek, amely támogatja ezeket a paramétereket.

  • Aszinkron programozási minták – Az aszinkron műveletek végrehajtásának három mintáját mutatja be: a feladatalapú aszinkron mintát (TAP), az aszinkron programozási modellt (APM) és az eseményalapú aszinkron mintát (EAP).
  • A tevékenységalapú aszinkron minta – A TAP implementálását három módon ismerteti: 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.
  • A feladatalapú aszinkron minta használata – Azt ismerteti, hogyan használhatja a feladatokat és a visszahívásokat a várakozás blokkolás nélküli elérésére.
  • Interop with Other Asynchronous Patterns and Types – Leírja, hogyan használható a TAP az aszinkron programozási modell (APM) és az eseményalapú aszinkron minta (EAP) megvalósításához.