Asynchronní vzor založený na úlohách (TAP) v .NET: Úvod a přehled

V .NET je asynchronní vzor založený na úlohách doporučeným vzorem asynchronního návrhu pro nový vývoj. Je založený na typech Task a Task<TResult> v System.Threading.Tasks oboru názvů, které reprezentují asynchronní operace.

Pojmenování, parametry a návratové typy

TAP používá jedinou metodu k reprezentaci zahájení a dokončení asynchronní operace. Tento přístup kontrastuje se vzorem asynchronního programovacího modelu (APM) a IAsyncResultasynchronním vzorem založeným na událostech (EAP). APM vyžaduje Begin a End metody. Protokol EAP vyžaduje metodu s příponou Async a také vyžaduje jednu nebo více událostí, typy delegátů pro obsluhu událostí a typy odvozené od EventArg. Asynchronní metody v TAP zahrnují příponu Async za názvem operace pro metody, které vracejí očekávané typy, například Task, Task<TResult>, ValueTaska ValueTask<TResult>. Například asynchronní Get operace, která vrací Task<String> hodnotu lze pojmenovat GetAsync. Pokud přidáváte metodu TAP do třídy, která už obsahuje název metody EAP s Async příponou, použijte místo toho příponu TaskAsync . Například pokud třída již má metodu GetAsync , použijte název GetTaskAsync. Pokud metoda spustí asynchronní operaci, ale nevrací očekávaný typ, jeho název by měl začínat znakem Begin, Startnebo jiným slovesem, který naznačuje, že tato metoda nevrací nebo vyvolá výsledek operace.

Metoda TAP vrátí buď System.Threading.Tasks.Task, nebo System.Threading.Tasks.Task<TResult> v závislosti na tom, zda odpovídající synchronní metoda vrací void nebo typ TResult.

Parametry metody TAP by měly odpovídat parametrům synchronního protějšku a měly by být poskytnuty ve stejném pořadí. Parametry out a ref jsou však z tohoto pravidla vyloučeny a měly by být zcela vynechány. Jakákoliv data, která vrací parametry out nebo ref, by měla být součástí TResult vráceného Task<TResult> a měla by využívat n-tici nebo vlastní datovou strukturu pro přizpůsobení více hodnot. Zvažte také přidání parametru CancellationToken i v případě, že synchronní protějšek metody TAP ho nenabízí.

Metody, které jsou určeny výhradně pro vytváření, manipulaci nebo kombinaci úloh (kde asynchronní záměr metody je jasný v názvu metody nebo v názvu typu, do kterého metoda patří), nemusí postupovat podle tohoto vzoru pojmenování. Takové metody se často označují jako kombinátory. Příklady kombinátorů zahrnují WhenAll a WhenAnya jsou popsány v části Použití předdefinovaných kombinátorů založených na úlohách článku Využívání asynchronního vzoru založeného na úlohách.

Příklady, jak se syntaxe TAP liší od syntaxe používané ve starších vzorech asynchronního programování, jako je asynchronní programovací model (APM) a asynchronní vzor založený na událostech (EAP), najdete v tématu Asynchronní programovací vzory.

Asynchronní chování, návratové typy a pojmenování

Klíčové async slovo nevynucuje asynchronní spuštění metody v jiném vlákně. Povolí await, a metoda se spustí synchronně, dokud nedosáhne neúplného čekatelného objektu. Pokud metoda nedosahuje nekompletního čekajícího objektu, může dokončit synchronně.

U většiny rozhraní API upřednostňujete tyto návratové typy:

  • Používá se Task pro asynchronní operace, které nevytvářely hodnotu.
  • Slouží Task<TResult> k asynchronním operacím, které vytvářejí hodnotu.
  • Používejte ValueTask nebo ValueTask<TResult> pouze v případech, kdy měření ukazují alokační tlak a kdy spotřebitelé můžou zvládnout dodatečná omezení využití.

Zachovat předvídatelné pojmenování TAP:

  • Příponu Async použijte pro metody, které vracejí očekávané typy.
  • Nepřidávejte Async k synchronním metodám.
  • Přidejte nové MethodNameAsync přetížení vedle existující metody `MethodName`. Neodebívejte ani nepřejmenovávejte synchronní rozhraní API. Díky tomu se volajícím dají migrovat vlastním tempem, aniž by došlo k zásadní změně.

Iniciování asynchronní operace

Asynchronní metoda, která je založená na TAP, může provést malé množství práce synchronně, například ověření argumentů a zahájení asynchronní operace, než vrátí výslednou úlohu. Udržujte synchronní práci na minimum, aby asynchronní metoda byla rychle vrácena. Mezi důvody rychlého vrácení patří:

  • Asynchronní metody můžete vyvolat z vláken uživatelského rozhraní a jakákoli dlouhotrvající synchronní práce může poškodit odezvu aplikace.
  • Souběžně můžete spustit několik asynchronních metod. Proto by jakákoli dlouhotrvající práce v synchronní části asynchronní metody mohla zpozdit zahájení jiných asynchronních operací, čímž se sníží výhody souběžnosti.

V některých případech je množství práce potřebné k dokončení operace menší než množství práce potřebné k asynchronnímu spuštění operace. Příkladem takového scénáře je čtení z datového proudu, kde operace čtení může být splněna daty, která jsou už uložená ve vyrovnávací paměti v paměti. V takových případech se operace může dokončit synchronně a může vrátit úkol, který je již dokončen.

Výjimky

Asynchronní metoda by měla vyvolat výjimku přímo z volání asynchronní metody pouze v reakci na chybu použití. Chyby použití by se nikdy neměly vyskytovat v produkčním kódu. Pokud například předáte odkaz null (Nothing v jazyce Visual Basic) jako jeden z argumentů metody způsobí chybový stav (obvykle reprezentovaný ArgumentNullException výjimkou), můžete upravit volající kód tak, aby se zajistilo, že odkaz null nikdy nepřejde. U všech ostatních chyb přiřaďte výjimky, ke kterým dochází při spuštění asynchronní metody, do vráceného úkolu, i když se asynchronní metoda dokončí synchronně před vrácením úkolu. Úloha obvykle obsahuje maximálně jednu výjimku. Pokud však úkol představuje více operací (například WhenAll), může být k jednomu úkolu přidruženo více výjimek.

Cílové prostředí

Při implementaci metody TAP můžete určit, kde dochází k asynchronnímu spuštění. Můžete se rozhodnout spustit úlohu ve fondu vláken, implementovat ji pomocí asynchronních vstupně-výstupních operací (bez vazby na vlákno pro většinu provádění operace), spustit ji na konkrétním vlákně (například vlákně uživatelského rozhraní) nebo použít libovolný počet potenciálních kontextů. Metoda TAP může dokonce nemít nic ke spuštění a může vrátit pouze Task výskyt podmínky, která se vyskytuje jinde v systému (například úloha představující data, která přicházejí do frontové datové struktury).

Volající metody TAP může blokovat čekání na dokončení metody TAP synchronním čekáním na výslednou úlohu nebo může po dokončení asynchronní operace spustit další kód (pokračování). Tvůrce kódu pokračování má kontrolu nad tím, kde se tento kód spouští. Kód pokračování můžete vytvořit explicitně prostřednictvím metod třídy Task (například ContinueWith) nebo implicitně pomocí podpory jazyka postavené na pokračováních (například await v jazyce C#, Await v Visual Basic, AwaitValue v jazyce F#).

Stav úkolu

Třída Task poskytuje životní cyklus pro asynchronní operace a tento cyklus je reprezentován výčtem TaskStatus . Chcete-li podporovat okrajové případy typů odvozených od Task a Task<TResult> a oddělit konstrukci od plánování, třída Task poskytuje metodu Start. Úlohy vytvořené veřejnými Task konstruktory jsou označovány jako studené úlohy, protože jejich životní cyklus začíná v neplánovaném Created stavu a jsou naplánovány pouze v případě, že Start jsou volány na těchto instancích.

Všechny ostatní úkoly začínají svůj životní cyklus v horkém stavu, což znamená, že asynchronní operace, které představují, jsou již zahájeny a jejich stav úkolu je hodnota výčtu jiná než TaskStatus.Created. Všechny úkoly vrácené z metod TAP musí být aktivovány. Pokud metoda TAP interně používá konstruktor úlohy k vytvoření instance úlohy, která má být vrácena, musí metoda TAP zavolat Start na objekt Task předtím, než ho vrátí. Příjemci metody TAP mohou bezpečně předpokládat, že vrácený úkol je aktivní a neměl by se pokoušet volat žádné Start vrácené Task metodou TAP. Volání Start na aktivním úkolu způsobí InvalidOperationException výjimku.

Pro pokyny k řešení druhu 'spustit a zapomenout' v souvislosti se životností a vlastnictvím po aktivaci úkolu si přečtěte téma Udržování asynchronních metod naživu.

Zrušení (volitelné)

V TAP je zrušení volitelné pro implementátory asynchronních metod i příjemce asynchronní metody. Pokud operace umožňuje zrušení, zveřejňuje přetížení asynchronní metody, která přijímá token zrušení (CancellationToken instance). Podle konvence má parametr název 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

Asynchronní operace monitoruje tento token kvůli žádostem o zrušení. Pokud obdrží žádost o zrušení, může se rozhodnout tuto žádost respektovat a operaci zrušit. Pokud má požadavek na zrušení za následek předčasné ukončení práce, vrátí metoda TAP úkol, který končí ve Canceled stavu; neexistuje žádný dostupný výsledek a není vyvolána žádná výjimka. Stav Canceled se považuje za konečný (dokončený) stav úkolu spolu se Faulted stavy a RanToCompletion stavy. Proto pokud je úkol ve Canceled stavu, jeho IsCompleted vlastnost vrátí true. Po dokončení úkolu ve Canceled stavu jsou všechna pokračování zaregistrovaná u úkolu naplánovaná nebo spuštěná, pokud není zadána možnost pokračování, jako NotOnCanceled je například možnost vyřazení z pokračování. Veškerý kód, který asynchronně čeká na zrušenou úlohu pomocí funkcí jazyka, se bude dál spouštět, ale obdrží výjimku OperationCanceledException nebo výjimku odvozenou z ní. Kód, který je synchronně blokován při čekání na úlohu prostřednictvím metod, jako jsou Wait a WaitAll, také pokračuje ve spuštění s výjimkou.

Pokud je vyžádáno zrušení tokenu před voláním metody TAP, která tento token přijímá, měla by metoda TAP vrátit Canceled úlohu. Pokud je však při spuštění asynchronní operace požadováno zrušení, asynchronní operace nemusí žádost o zrušení přijmout. Vrácený úkol by měl končit ve Canceled stavu pouze v případě, že operace skončí v důsledku požadavku na zrušení. Pokud je požadováno zrušení, ale výsledek nebo výjimka je stále vytvořena, úkol by měl končit ve stavu RanToCompletion nebo Faulted.

Pro asynchronní metody, které chtějí především zpřístupnit schopnost být zrušeny, nemusíte poskytovat přetížení, které nepřijímá rušící token. U metod, které nelze zrušit, neposkytujte přetížení, která přijímají token zrušení; to pomůže volajícímu zjistit, zda je cílová metoda skutečně zrušitelná. Spotřebitelský kód, který nechce zrušení, může volat metodu, která přijímá CancellationToken a poskytuje None jako hodnotu argumentu. None je funkčně ekvivalentní výchozímu CancellationToken.

Vykazování průběhu (volitelné)

Některé asynchronní operace těží z poskytování oznámení o průběhu. Tato oznámení se obvykle používají k aktualizaci uživatelského rozhraní s informacemi o průběhu asynchronní operace.

V rámci TAP řiďte průběh prostřednictvím IProgress<T> rozhraní. Toto rozhraní předejte asynchronní metodě jako parametr, obvykle pojmenovaný progress. Když při volání asynchronní metody poskytnete rozhraní pro sledování průběhu, pomáháte eliminovat souběh podmínek, které vznikají v důsledku nesprávného použití. Tyto podmínky souběhu nastanou, když jsou obslužné rutiny událostí nesprávně zaregistrovány po spuštění operace a minou aktualizace. Důležitější je, že rozhraní průběhu podporuje různé implementace průběhu, jak je určeno využíváním kódu. Například využívání kódu se může starat pouze o nejnovější aktualizaci průběhu, nebo může chtít ukládat všechny aktualizace do vyrovnávací paměti, vyvolat akci pro každou aktualizaci nebo řídit, zda je vyvolání zařazováno do určitého vlákna. Všechny tyto možnosti jsou dosažitelné pomocí různých implementací rozhraní, které jsou přizpůsobené potřebám konkrétního příjemce. Stejně jako u zrušení by implementace TAP měly poskytovat IProgress<T> parametr pouze v případě, že rozhraní API podporuje oznámení o průběhu.

Pokud metoda ReadAsync, kterou jsme probírali dříve v tomto článku, může hlásit průběh jako počet dosud přečtených bajtů, může být zpětné volání pro sledování průběhu rozhraním IProgress<T>.

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

FindFilesAsync Pokud metoda vrátí seznam všech souborů, které splňují určitý vzor hledání, zpětné volání průběhu může poskytnout odhad procenta dokončené práce a aktuální sadu částečných výsledků. Tyto informace by mohlo poskytnout buď jako n-tice:

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

nebo s datovým typem, který je specifický pro rozhraní 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))

V druhém případě je speciální datový typ obvykle příponou ProgressInfo.

Pokud implementace TAP poskytují přetížení, které akceptují parametr progress, musí umožnit, aby argument byl null. Pokud předáte null, nebude hlášen žádný průběh. Implementace TAP by měly hlásit průběh Progress<T> objektu synchronně, což umožňuje asynchronní metodě rychle poskytnout průběh. Umožňuje také příjemci průběhu určit, jak a kde nejlépe zpracovávat informace. Instance sledování průběhu se například může rozhodnout usměrňovat zpětná volání a vyvolávat události v zachyceném kontextu synchronizace.

Implementace pro IProgress<T>

.NET poskytuje Progress<T> třídu, která implementuje IProgress<T>. Třída Progress<T> je deklarována takto:

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

Instance Progress<T> zveřejňuje ProgressChanged událost, která se vyvolá pokaždé, když asynchronní operace hlásí aktualizaci průběhu. Událost ProgressChanged je vyvolána u objektu SynchronizationContext, který instance Progress<T> zachytí při svém vytvoření. Pokud není k dispozici žádný kontext synchronizace, použije se výchozí kontext, který cílí na fond vláken. Můžete zaregistrovat zpracovatele k této události. Pro usnadnění můžete konstruktoru Progress<T> poskytnout také jednu obslužnou proceduru. Tato obslužná rutina se chová stejně jako obslužná rutina pro ProgressChanged událost. Aktualizace průběhu jsou vyvolány asynchronně, aby se zabránilo zpoždění asynchronní operace během zpracovávání událostí. Jiná IProgress<T> implementace by se mohla rozhodnout použít jinou sémantiku.

Volba přetížení, která se mají poskytnout

Pokud implementace TAP používá volitelné CancellationToken i volitelné IProgress<T> parametry, může to potenciálně vyžadovat až čtyři přetížení:

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

Mnoho implementací TAP neposkytuje ale možnosti zrušení ani sledování průběhu, takže vyžadují jednu metodu.

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

Pokud implementace TAP podporuje buď zrušení, nebo průběh, ale ne obojí, může poskytnout dvě přetížení:

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

Pokud implementace TAP podporuje zrušení i průběh, může zveřejnit všechna čtyři přetížení. Může však poskytovat pouze následující dvě položky:

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

Aby vývojáři mohli nahradit tyto dvě chybějící přechodné kombinace, mohou předat parametrům cancellationToken a progress výchozí hodnoty None a null.

Pokud očekáváte, že každé použití metody TAP podporuje zrušení nebo průběh, můžete vynechat přetížení, která nepřijímají příslušný parametr.

Pokud se rozhodnete zveřejnit více přetížení, aby zrušení nebo postup byl volitelný, přetížení, která nepodporují zrušení nebo postup, by se měla chovat, jako by předala hodnotu None pro zrušení nebo null pro postup přetížení, které tyto parametry podporuje.