Implementace asynchronního vzoru založeného na úlohách

Asynchronní vzor založený na úkolech (TAP) můžete implementovat třemi způsoby: pomocí kompilátorů jazyka C# a Visual Basic v sadě Visual Studio, ručně nebo kombinací obou metod. Jednotlivé metody jsou podrobně popsány v následujících částech. Model TAP můžete použít k implementaci asynchronních operací vázaných na výpočty i vstupně-výstupní operace. Část Úlohy popisuje jednotlivé typy operací.

Generování metod TAP

Použití kompilátorů

Počínaje rozhraním .NET Framework 4.5 je každá metoda, která je přiřazena klíčovým slovem async (Async v jazyce Visual Basic), považována za asynchronní metodu a kompilátory jazyka C# a Visual Basic provádějí potřebné transformace k asynchronní implementaci metody pomocí TAP. Asynchronní metoda by měla vrátit buď objekt System.Threading.Tasks.Task, nebo objekt System.Threading.Tasks.Task<TResult>. V případě druhé funkce by tělo funkce mělo vrátit TResulta kompilátor zajistí, aby byl tento výsledek zpřístupněn prostřednictvím výsledného objektu úkolu. Podobně všechny výjimky, které se neošetřují v těle metody, jsou zařazovány do výstupní úlohy a způsobí, že výsledný úkol skončí ve TaskStatus.Faulted stavu. Výjimkou tohoto pravidla je, že dojde k neošetřenému OperationCanceledException (nebo odvozeného typu) v takovém případě, že výsledný úkol skončí ve TaskStatus.Canceled stavu.

Ruční generování metod TAP

Vzor TAP můžete implementovat ručně a dosáhnout tak lepší kontroly nad implementací. Kompilátor spoléhá na veřejnou oblast vystavenou z oboru názvů System.Threading.Tasks s podporou typů v oboru názvů System.Runtime.CompilerServices. Při vlastní implementaci vzoru TAP vytvoříte objekt TaskCompletionSource<TResult>, provedete asynchronní operaci a po jejím dokončení zavoláte metodu SetResult, SetException nebo SetCanceled anebo verzi Try jedné z těchto metod. Při ruční implementaci metody TAP musíte dokončit výsledný úkol po dokončení zastoupené asynchronní operace. Příklad:

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

Hybridní přístup

Pravděpodobně pro vás bude užitečné, pokud implementujete vzor TAP ručně, ale delegujete základní logiku pro implementaci na kompilátor. Můžete například chtít použít hybridní přístup, když chcete ověřit argumenty mimo asynchronní metodu vygenerovanou kompilátorem, aby výjimky mohly uniknout přímému volajícímu metody, a ne vystavit je prostřednictvím objektu System.Threading.Tasks.Task :

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

Dalším užitečným použitím takového delegování je implementace optimalizace rychlé cesty a vrácení úkolu v mezipaměti.

Úlohy

Jako metody TAP lze implementovat výpočetní i vstupně-výstupní asynchronní operace. Jsou-li však metody TAP vystaveny veřejně z knihovny, měly by být poskytnuty pouze pro úkoly, které se týkají vstupně-výstupních operací (mohou také zahrnovat výpočet, ale neměly by být čistě výpočetní). Pokud je metoda čistě vázaná na výpočetní prostředky, měla by být vystavena pouze jako synchronní implementace. Kód, který ho využívá, se pak může rozhodnout, jestli se má zabalit vyvolání této synchronní metody do úlohy, aby se práce přesměrovalo do jiného vlákna nebo aby se dosáhlo paralelismu. A pokud je metoda vázaná na vstupně-výstupní operace, měla by být vystavena pouze jako asynchronní implementace.

Výpočetní úkoly vázané na výpočetní prostředky

Třída System.Threading.Tasks.Task je nejvhodnější pro zastoupení výpočetně náročných operací. Ve výchozím nastavení využívá speciální podporu v rámci třídy ThreadPool za účelem poskytování účinného provádění a zároveň poskytuje významnou kontrolu nad tím, kdy, kde a jakým způsobem lze provádět asynchronní výpočty.

Úlohy vázané na výpočetní výkon můžete vygenerovat následujícími způsoby:

  • V rozhraní .NET Framework 4.5 a novějších verzích (včetně .NET Core a .NET 5+) použijte statickou Task.Run metodu jako zástupce TaskFactory.StartNew. Metodu Run můžete použít pro snadné spouštění výpočetního úkolu, který se zaměřuje na fond vláken. Toto je upřednostňovaný mechanismus pro spuštění úlohy vázané na výpočetní výkon. Používejte StartNew přímo pouze v případech, kdy chcete mít nad úkolem přesnější kontrolu.

  • V rozhraní .NET Framework 4 použijte metodu TaskFactory.StartNew , která přijímá delegáta (obvykle Action<T> a) Func<TResult>ke spuštění asynchronně. Pokud poskytnete delegát typu Action<T>, vrátí metoda objekt System.Threading.Tasks.Task, který představuje asynchronní provádění tohoto delegátu. Pokud zadáte delegát typu Func<TResult>, vrátí metoda objekt System.Threading.Tasks.Task<TResult>. Přetížení metody StartNew přijímá token zrušení (CancellationToken), možnosti vytvoření úkolu (TaskCreationOptions) a plánovač úkolů (TaskScheduler), které poskytují detailní kontrolu nad plánováním a prováděním úkolu. Instance objektu pro vytváření úkolů, která je určena pro aktuální plánovač úkolů, je k dispozici jako statická vlastnost (Factory) třídy Task, například Task.Factory.StartNew(…).

  • Pokud chcete generovat a naplánovat úlohu samostatně, použijte konstruktory Task typu a Start metodu. Veřejné metody smí vracet pouze úkoly, které již byly zahájeny.

  • Použijte přetížení metody Task.ContinueWith. Tato metoda vytvoří nový úkol, jehož spuštění je naplánováno po dokončení jiného úkolu. Některá přetížení ContinueWith přijímají token zrušení, možnosti pokračování a plánovač úkolů pro lepší kontrolu nad plánováním a prováděním úkolu pokračování.

  • Použijte metody TaskFactory.ContinueWhenAll a TaskFactory.ContinueWhenAny metody. Tyto metody vytvoří nový úkol, který je naplánován na dobu, kdy skončí všechny nebo libovolná zadaná sada úkolů. Tyto metody také poskytují přetížení pro řízení plánování a provádění těchto úloh.

V případě výpočetních úkolů může systém zabránit spuštění naplánovaného úkolu, pokud obdrží požadavek na zrušení před spuštěním úkolu. Pokud je takto poskytován token zrušení (CancellationToken), můžete tento token předat asynchronnímu kódu, který tento token sleduje. Token můžete také poskytnout jedné z výše uvedených metod, jako je StartNew nebo Run, aby modul runtime typu Task mohl tento token také sledovat.

Zvažte například asynchronní metodu, která vytváří obrázek. Tělo úkolu může dotazovat token zrušení tak, aby se kód mohl předčasně ukončit, pokud bude požadavek na zrušení doručen během vykreslování. Pokud bude navíc požadavek na zrušení doručen před spuštěním vykreslování, budete chtít zabránit operaci vykreslování:

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

Výpočetní úkoly skončí ve stavu Canceled, pokud je splněna alespoň jedna z následujících podmínek:

  • Dříve než úkol přejde do stavu CancellationToken, je požadavek na zrušení doručen prostřednictvím objektu StartNew, který je k dispozici ve formě argumentu metody vytváření (například metody Run nebo Running).

  • Výjimka OperationCanceledException zůstane v těle takového úkolu nezpracovaná. Tato výjimka obsahuje stejný token CancellationToken, který je předán úkolu. Tento token dokazuje, že je požadováno zrušení.

Pokud uvnitř těla úkolu zůstane další nezpracovaná výjimka, skončí tento úkol ve stavu Faulted a pokusy o čekání na úkol nebo přístup k výsledku úkolu způsobí vyvolání výjimky.

Vstupně-výstupní úkoly

Chcete-li vytvořit úkol, pro který by po celou dobu provádění nemělo existovat podkladové vlákno, použijte typ TaskCompletionSource<TResult>. Tento typ vystavuje vlastnost Task, která vrací přidruženou instanci typu Task<TResult>. Životní cyklus tohoto úkolu je řízen metodami typu TaskCompletionSource<TResult>, jako jsou SetResult, SetException, SetCanceled, a jejich variantami TrySet.

Předpokládejte, že chcete vytvořit úkol, který bude dokončen po určitém časovém období. Například budete chtít odložit aktivitu v uživatelském rozhraní na pozdější dobu. Třída System.Threading.Timer již poskytuje schopnost asynchronního vyvolání delegátu po zadaném časovém intervalu a pomocí typu TaskCompletionSource<TResult> lze umístit typ Task<TResult> před časovač, například:

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

Metoda Task.Delay je k dispozici pro tento účel a můžete ji použít v jiné asynchronní metodě, například k implementaci asynchronní smyčky dotazování:

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

Třída TaskCompletionSource<TResult> nemá neobecný protějšek. Typ Task<TResult> je však odvozen od typu Task, takže obecný objekt TaskCompletionSource<TResult> můžete použít pro vstupně-výstupní metody, které jednoduše vrátí úkol. Chcete-li tuto operaci provést, můžete použít prostředek s fiktivním typem TResult (Boolean je dobrá výchozí volba, ale pokud se obáváte, že uživatel typu Task ji bude přetypovávat dolů na typ Task<TResult>, lze namísto toho použít privátní typ TResult). Například metoda Delay v předchozím příkladu vrátí aktuální čas s výsledným posunem (Task<DateTimeOffset>). Pokud je výsledná hodnota nepotřebná, může být metoda kódována spíše takto (všimněte si změny návratového typu a změny argumentu na hodnotu TrySetResult):

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

Smíšené výpočetní úlohy vázané na vstupně-výstupní operace

Asynchronní metody nejsou omezeny pouze na výpočetní nebo vstupně-výstupní operace, ale mohou představovat kombinaci obou metod. Ve skutečnosti je větší počet asynchronních operací často sloučen do větších smíšených operací. Například metoda RenderAsync v předchozím příkladu provedla výpočetně náročnou operaci za účelem vykreslení obrázku na základě vstupu proměnné imageData. Zdrojem této proměnné imageData může být webová služba s asynchronním přístupem:

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

Tento příklad také znázorňuje, jakým způsobem lze jeden token zrušení zřetězit prostřednictvím několika asynchronních operací. Další informace najdete v části využití zrušení v části Využívání asynchronního vzoru založeného na úlohách.

Viz také