Asynchroon patroon (TAP) op basis van taken in .NET: Inleiding en overzicht

In .NET is het op taken gebaseerde asynchrone patroon het aanbevolen asynchrone ontwerppatroon voor nieuwe ontwikkeling. Deze is gebaseerd op de Task en Task<TResult> typen in de System.Threading.Tasks naamruimte, die asynchrone bewerkingen vertegenwoordigen.

Naamgeving, parameters en retourtypen

TAP gebruikt één methode om de start en voltooiing van een asynchrone bewerking aan te geven. Deze benadering contrasteert met zowel het Asynchrone programmeermodelpatroon (APM of IAsyncResult) als het op gebeurtenissen gebaseerde Asynchrone patroon (EAP). APM vereist Begin en End methoden. EAP vereist een methode met het Async-achtervoegsel en daarnaast vereist het een of meerdere gebeurtenissen, delegatetypen voor gebeurtenishandlers en EventArg-afgeleide typen. Asynchrone methoden in TAP bevatten het Async achtervoegsel na de naam van de bewerking voor methoden die te verwachten typen retourneren, zoals Task, Task<TResult>, ValueTasken ValueTask<TResult>. Een asynchrone Get bewerking die een Task<String> retourneert, kan bijvoorbeeld GetAsync heten. Als u een TAP-methode toevoegt aan een klasse die al een EAP-methodenaam met het Async achtervoegsel bevat, gebruikt u in plaats daarvan het achtervoegsel TaskAsync . Als de klasse bijvoorbeeld al een GetAsync methode heeft, gebruikt u de naam GetTaskAsync. Als een methode een asynchrone bewerking start, maar geen wachtbaar type retourneert, moet de naam beginnen met Begin, Startof een ander werkwoord om aan te geven dat deze methode het resultaat van de bewerking niet retourneert of genereert.

Een TAP-methode retourneert een System.Threading.Tasks.Task of een System.Threading.Tasks.Task<TResult>, afhankelijk van of de bijbehorende synchrone methode void of een type TResult retourneert.

De parameters van een TAP-methode moeten overeenkomen met de parameters van de synchrone tegenhanger en moeten in dezelfde volgorde worden opgegeven. Parameters out en ref vallen echter buiten deze regel en moeten volledig vermeden worden. Gegevens die door een out- of ref-parameter worden geretourneerd, moeten in plaats daarvan deel uitmaken van de door Task<TResult> geretourneerde TResult en moeten een tuple of een aangepaste gegevensstructuur gebruiken om meerdere waarden te accommoderen. Overweeg ook om een CancellationToken parameter toe te voegen, zelfs als de synchrone tegenhanger van de TAP-methode er geen biedt.

Methoden die uitsluitend zijn gewijd aan het maken, bewerken of combineren van taken (waarbij de asynchrone intentie van de methode duidelijk is in de naam van de methode of in de naam van het type waartoe de methode behoort), hoeven dit naamgevingspatroon niet te volgen. Dergelijke methoden worden vaak combinaties genoemd. Voorbeelden van combinatoren zijn onder andere WhenAll en WhenAny, en worden besproken in de sectie 'Het gebruiken van ingebouwde, taak-gebaseerde combinatoren' van het artikel 'Het gebruik van het taak-gebaseerde asynchrone patroon'.

Zie Asynchrone programmeerpatronen voor voorbeelden van hoe de TAP-syntaxis verschilt van de syntaxis die wordt gebruikt in verouderde asynchrone programmeerpatronen, zoals het Asynchrone programmeermodel (APM) en het op gebeurtenissen gebaseerde Asynchrone patroon (EAP).

Asynchroon gedrag, retourtypen en naamgeving

Het async trefwoord dwingt geen methode om asynchroon uit te voeren op een andere thread. Het stelt await in staat, en de methode wordt synchroon uitgevoerd totdat deze een onvolledige wachtbare heeft bereikt. Als de methode een onvolledige wachtbare niet bereikt, kan deze synchroon worden voltooid.

Voor de meeste API's geeft u de voorkeur aan deze retourtypen:

  • Gebruik Task deze indeling voor asynchrone bewerkingen die geen waarde produceren.
  • Gebruiken Task<TResult> voor asynchrone bewerkingen die een waarde produceren.
  • Gebruik ValueTask of ValueTask<TResult> alleen wanneer metingen toewijzingsdruk tonen en wanneer consumenten de extra gebruiksbeperkingen kunnen afhandelen.

Houd TAP-naamgeving voorspelbaar:

  • Gebruik het Async achtervoegsel voor methoden die wachtbare typen retourneren.
  • Voeg Async niet toe aan synchrone methoden.
  • Voeg de nieuwe MethodNameAsync overload toe naast de bestaande MethodName. Verwijder of wijzig de naam van de synchrone API niet. Door beide bellers in hun eigen tempo te laten migreren zonder een belangrijke wijziging.

Een asynchrone bewerking starten

Een asynchrone methode die is gebaseerd op TAP, kan een kleine hoeveelheid werk synchroon uitvoeren, zoals het valideren van argumenten en het initiëren van de asynchrone bewerking, voordat de resulterende taak wordt geretourneerd. Houd synchroon werk tot een minimum, zodat de asynchrone methode snel kan terugkeren. Redenen voor een snelle terugkeer zijn onder andere:

  • U kunt asynchrone methoden aanroepen vanuit gebruikersinterfacethreads en langlopende synchrone werkzaamheden kunnen de reactiesnelheid van de toepassing schaden.
  • U kunt meerdere asynchrone methoden gelijktijdig starten. Daarom kan elk langlopend werk in het synchrone gedeelte van een asynchrone methode de start van andere asynchrone bewerkingen vertragen, waardoor de voordelen van gelijktijdigheid afnemen.

In sommige gevallen is de hoeveelheid werk die nodig is om de bewerking te voltooien, kleiner dan de hoeveelheid werk die nodig is om de bewerking asynchroon te starten. Lezen vanuit een stroom waar de leesbewerking kan worden voldaan door gegevens die al in het geheugen zijn gebufferd, is een voorbeeld van een dergelijk scenario. In dergelijke gevallen kan de bewerking synchroon worden voltooid en wordt mogelijk een taak geretourneerd die al is voltooid.

Uitzonderingen

Een asynchrone methode moet rechtstreeks vanuit de asynchrone methodeaanroep een uitzondering genereren als reactie op een gebruiksfout. Gebruiksfouten mogen nooit optreden in productiecode. Als u bijvoorbeeld een null-verwijzing (Nothing in Visual Basic) doorgeeft als een van de argumenten van de methode een foutstatus veroorzaakt (meestal vertegenwoordigd door een ArgumentNullException uitzondering), kunt u de aanroepende code wijzigen om ervoor te zorgen dat er nooit een null-verwijzing wordt doorgegeven. Voor alle andere fouten wijst u uitzonderingen toe die optreden wanneer een asynchrone methode wordt uitgevoerd voor de geretourneerde taak, zelfs als de asynchrone methode synchroon wordt voltooid voordat de taak wordt geretourneerd. Normaal gesproken bevat een taak maximaal één uitzondering. Als de taak echter meerdere bewerkingen vertegenwoordigt (bijvoorbeeld WhenAll), kunnen meerdere uitzonderingen worden gekoppeld aan één taak.

Doelomgeving

Wanneer u een TAP-methode implementeert, kunt u bepalen waar asynchrone uitvoering plaatsvindt. U kunt ervoor kiezen om de workload op de threadgroep uit te voeren, deze te implementeren met behulp van asynchrone I/O (zonder gebonden te zijn aan een thread voor het merendeel van de uitvoering van de bewerking), deze uit te voeren op een specifieke thread (zoals de UI-thread) of een willekeurig aantal mogelijke contexten te gebruiken. Een TAP-methode kan zelfs niets hebben om uit te voeren en retourneert mogelijk alleen een Task voorwaarde die ergens anders in het systeem voorkomt (bijvoorbeeld een taak die gegevens vertegenwoordigt die in een gegevensstructuur in de wachtrij terechtkomt).

De aanroeper van de TAP-methode kan het wachten blokkeren totdat de TAP-methode is voltooid door synchroon te wachten op de resulterende taak of extra code (vervolgcode) uit te voeren wanneer de asynchrone bewerking is voltooid. De maker van de vervolgcode heeft controle over waar die code wordt uitgevoerd. U kunt de vervolgcode expliciet maken via methoden voor de klasse Task (bijvoorbeeld ContinueWith) of impliciet, met behulp van taalondersteuning die is gebouwd op voortzettingen (bijvoorbeeld await in C#, Await in Visual Basic, AwaitValue in F#).

Taakstatus

De Task klasse biedt een levenscyclus voor asynchrone bewerkingen en die cyclus wordt vertegenwoordigd door de TaskStatus opsomming. Ter ondersteuning van randgevallen van typen die zijn afgeleid van Task en Task<TResult>, en om de scheiding van constructie van planning te ondersteunen, stelt de Task klasse een Start methode beschikbaar. Taken die door de publieke Task constructors worden gemaakt, worden koude taken genoemd, omdat ze hun levensloop in de onbekende Created toestand beginnen en pas worden gepland wanneer Start op deze exemplaren wordt aangeroepen.

Alle andere taken beginnen hun levenscyclus in een dynamische status, wat betekent dat de asynchrone bewerkingen die ze vertegenwoordigen al worden gestart en hun taakstatus een andere opsommingswaarde is dan TaskStatus.Created. Alle taken die worden geretourneerd vanuit TAP-methoden, moeten worden geactiveerd. Als een TAP-methode intern gebruikmaakt van de constructor van een taak om de taak te instantiëren die moet worden geretourneerd, moet de TAP-methode het Start object aanroepen Task voordat het wordt geretourneerd. Consumenten van een TAP-methode kunnen er veilig van uitgaan dat de geretourneerde taak actief is en niet proberen Start aan te roepen op een Task die geretourneerd wordt vanuit een TAP-methode. Het aanroepen van een actieve taak Start resulteert in een InvalidOperationException uitzondering.

Zie Asynchrone methoden actief houden voor richtlijnen over fire-and-forget-levens- en eigendomsproblemen na activering van taken.

Annulering (optioneel)

In TAP is annulering optioneel voor zowel asynchrone methode-implementers als asynchrone methodegebruikers. Als een bewerking annulering toestaat, wordt er een overbelasting weergegeven van de asynchrone methode die een annuleringstoken (CancellationToken instantie) accepteert. Volgens conventie heeft de parameter de naam 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

De asynchrone bewerking bewaakt dit token op annuleringsaanvragen. Als het een annuleringsaanvraag ontvangt, kan het ervoor kiezen deze aanvraag te respecteren en de bewerking te annuleren. Als de annuleringsaanvraag voortijdig eindigt, retourneert de TAP-methode een taak die eindigt op de Canceled status. Er is geen beschikbaar resultaat en er wordt geen uitzondering gegenereerd. De Canceled status wordt beschouwd als een definitieve (voltooide) status voor een taak, samen met de Faulted statussen.RanToCompletion Als een taak zich in de Canceled-status bevindt, waardoor de IsCompleted-eigenschap true retourneert. Wanneer een taak in de Canceled status is voltooid, worden eventuele vervolgtaken die zijn geregistreerd bij de taak gepland of uitgevoerd, tenzij een vervolgoptie zoals NotOnCanceled is opgegeven om de vervolgactie over te slaan. Alle code die asynchroon wacht op een geannuleerde taak via het gebruik van taalfuncties, blijft actief, maar ontvangt een OperationCanceledException of een uitzondering die daaruit is afgeleid. Code die synchroon wordt geblokkeerd terwijl op de taak wordt gewacht via methoden zoals Wait en WaitAll die ook worden uitgevoerd met een uitzondering.

Als een annuleringstoken annulering aanvraagt voordat de TAP-methode die dat token accepteert, wordt aangeroepen, moet de TAP-methode een Canceled taak retourneren. Als er echter annulering wordt aangevraagd terwijl de asynchrone bewerking wordt uitgevoerd, hoeft de asynchrone bewerking de annuleringsaanvraag niet te accepteren. De geretourneerde taak zou alleen in de Canceled-toestand moeten eindigen als de bewerking eindigt als gevolg van de annuleringsaanvraag. Als annulering wordt aangevraagd, maar er nog steeds een resultaat of een uitzondering wordt geproduceerd, moet de taak eindigen in de RanToCompletion of Faulted toestand.

Voor asynchrone methoden die in de eerste plaats de mogelijkheid willen bieden om geannuleerd te worden, hoeft u geen overload op te geven die geen annuleringstoken accepteert. Voor methoden die niet kunnen worden geannuleerd, biedt u geen overbelastingen die een annuleringstoken accepteren; dit helpt de aanroeper aan te geven of de doelmethode daadwerkelijk kan worden geannuleerd. Consumentencode die geen annulering wenst, kan een methode aanroepen die een CancellationToken methode accepteert en als argumentwaarde opgeeft None . None is functioneel gelijk aan de standaardwaarde CancellationToken.

Voortgangsrapportage (optioneel)

Sommige asynchrone bewerkingen profiteren van het bieden van voortgangsmeldingen. Gebruik deze meldingen doorgaans om een gebruikersinterface bij te werken met informatie over de voortgang van de asynchrone bewerking.

In TAP kunt u de voortgang via een IProgress<T> interface afhandelen. Geef deze interface door aan de asynchrone methode als een parameter, meestal met de naam progress. Wanneer u de voortgangsinterface op het moment van het aanroepen van de asynchrone methode opgeeft, kunt u racevoorwaarden elimineren die het gevolg zijn van onjuist gebruik. Deze racevoorwaarden treden op wanneer event-handlers niet correct zijn geregistreerd nadat de bewerking is gestart en daardoor updates missen. Belangrijker is dat de voortgangsinterface verschillende implementaties van de voortgang ondersteunt, zoals wordt bepaald door de verbruikende code. De consumerende code kan bijvoorbeeld alleen interesse hebben in de meest recente voortgangsupdate, of het kan zijn dat het alle updates wil bufferen, een actie voor elke update wil uitvoeren, of wil bepalen of de aanroep naar een bepaalde thread wordt gemarshald. Al deze opties zijn haalbaar met behulp van verschillende implementaties van de interface, aangepast aan de behoeften van de specifieke consument. Net als bij annulering moeten TAP-implementaties alleen een IProgress<T> parameter opgeven als de API voortgangsmeldingen ondersteunt.

Als de ReadAsync methode die eerder in dit artikel is besproken, bijvoorbeeld tussenliggende voortgang kan rapporteren in de vorm van het aantal bytes dat tot nu toe is gelezen, kan de callback van de voortgang een IProgress<T> interface zijn:

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

Als een FindFilesAsync methode een lijst retourneert van alle bestanden die voldoen aan een bepaald zoekpatroon, kan de callback van de voortgang een schatting geven van het percentage voltooid werk en de huidige set gedeeltelijke resultaten. Het kan deze informatie verstrekken met ofwel een tuple:

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))

of met een gegevenstype dat specifiek is voor de API:

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))

In het laatste geval wordt het speciale gegevenstype meestal voorzien ProgressInfovan een achtervoegsel.

Als TAP-implementaties overbelastingen bieden die een progress-parameter accepteren, moeten ze toestaan dat het argument null kan zijn. Als null wordt gepasseerd, zal er geen voortgang worden gerapporteerd. TAP-implementaties moeten de voortgang van het Progress<T> object synchroon rapporteren, waardoor de asynchrone methode snel voortgang kan bieden. Daarnaast kan de consument van de voortgang bepalen hoe en waar de informatie het beste kan worden verwerkt. Het voortgangsexemplaar kan er bijvoorbeeld voor kiezen om callback-functies aan te sturen en evenementen op te roepen in een vastgelegde synchronisatiecontext.

IProgress<T>-implementaties

.NET biedt de Progress<T> klasse, die implementeert IProgress<T>. De Progress<T> klasse wordt als volgt gedeclareerd:

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

Een exemplaar van Progress<T> stelt een ProgressChanged gebeurtenis beschikbaar, die elke keer wordt geactiveerd wanneer de asynchrone bewerking een voortgangsupdate rapporteert. De ProgressChanged gebeurtenis wordt gegenereerd op het SynchronizationContext object dat door het Progress<T> exemplaar wordt vastgelegd wanneer deze wordt geïnstantieerd. Als er geen synchronisatiecontext beschikbaar is, wordt een standaardcontext gebruikt die is gericht op de threadpool. U kunt handlers registreren bij deze gebeurtenis. Voor het gemak kunt u ook één handler aan de Progress<T> constructor leveren. Deze handler gedraagt zich net als een gebeurtenis-handler voor de ProgressChanged gebeurtenis. Voortgangsupdates worden asynchroon doorgegeven om te voorkomen dat de asynchrone operatie wordt vertraagd terwijl eventhandlers worden uitgevoerd. Een andere IProgress<T> implementatie kan ervoor kiezen om verschillende semantiek toe te passen.

Kiezen van de overloads om te voorzien

Als een TAP-implementatie zowel de optionele als optionele CancellationTokenIProgress<T> parameters gebruikt, kan dit maximaal vier overbelastingen vereisen:

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

Veel TAP-implementaties bieden echter geen mogelijkheden voor annulering of voortgang, dus ze vereisen één methode:

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

Als een TAP-implementatie annulering of voortgang ondersteunt, maar niet beide, kan dit twee overbelastingen bieden:

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

Als een TAP-implementatie zowel annulering als voortgang ondersteunt, kunnen alle vier de overbelastingen zichtbaar zijn. Het kan echter slechts de volgende twee bieden:

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

Om te compenseren voor de twee ontbrekende tussenliggende combinaties kunnen ontwikkelaars None of een standaard CancellationToken voor de cancellationToken-parameter en null voor de progress-parameter doorgeven.

Als u verwacht dat elk gebruik van de TAP-methode ondersteuning biedt voor annulering of voortgang, kunt u de overloads weglaten die geen relevante parameter accepteren.

Als u besluit om meerdere overloads beschikbaar te maken om annulering of voortgang optioneel te maken, moeten de overloads die geen ondersteuning bieden voor annulering of voortgang zich gedragen alsof ze None doorgeven voor annulering of null voor voortgang, aan de overload die deze parameters wel ondersteunt.