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

Asynchronní vzor založený na úlohách (TAP) můžete implementovat třemi způsoby: pomocí kompilátorů C# a Visual Basic v Visual Studio, ručně nebo kombinací kompilátoru a ručních 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 .NET Framework 4.5 se každá metoda, která je přiřazena klíčovým slovem async (Async v Visual Basic), považuje za asynchronní metodu. Kompilátory C# a Visual Basic provádějí potřebné transformace pro asynchronní implementaci metody pomocí TAP. Asynchronní metoda by měla vrátit objekt System.Threading.Tasks.Task nebo System.Threading.Tasks.Task<TResult> objekt. 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 z tohoto pravidla je situace, kdy OperationCanceledException (nebo odvozený typ) není ošetřen, pak výsledný úkol končí ve stavu TaskStatus.Canceled.

Zahájení úlohy a ukončení/likvidace úlohy

Používejte Start pouze pro úlohy explicitně vytvořené pomocí konstruktoru Task, které jsou stále ve stavu Created. Veřejné metody TAP by měly vracet aktivní úkoly, takže volající by neměli volat Start.

Ve většině kódu TAP neodhazujte úkoly. A Task v typickém případě neudržuje nespravované prostředky, přičemž likvidace každého úkolu přidává režijní zatížení bez praktické výhody. Zlikvidujte pouze v případě, že konkrétní rozhraní API nebo měření ukazují, že je to potřeba.

Pokud spustíte práci na pozadí, která přesahuje okamžitou cestu volání, ponechte vlastnictví explicitní a sledujte její dokončení. Další pokyny najdete v tématu Udržování asynchronních metod naživu.

Ruční generování metod TAP

Vzor TAP můžete implementovat ručně, abyste měli lepší kontrolu nad implementací. Kompilátor spoléhá na veřejně dostupné části z oboru názvů System.Threading.Tasks a z podpůrných typů v oboru názvů System.Runtime.CompilerServices. Pokud chcete implementovat TAP sami, vytvoříte objekt TaskCompletionSource<TResult>, provedete asynchronní operaci, a po jejím dokončení zavoláte metody SetResult, SetException nebo SetCanceled, nebo verzi Try jedné z těchto metod. Když implementujete metodu TAP ručně, musíte dokončit výslednou úlohu po dokončení reprezentované asynchronní operace. Například:

static class StreamExtensions
{
    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;
    }
}
Module StreamExtensions
    <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
End Module

Hybridní přístup

Může být užitečné implementovat vzor TAP ručně, ale delegovat základní logiku implementace do kompilátoru. 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 utéct přímo volajícímu metody místo toho, aby byly vystaveny prostřednictvím objektu System.Threading.Tasks.Task :

class Calculator
{
    private int value = 0;

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

    private async Task<int> MethodAsyncInternal(string input)
    {
        // code that uses await goes here
        await Task.Delay(1);
        return value;
    }
}
Class Calculator
    Private value As Integer = 0

    Public Function MethodAsync(input As String) As Task(Of Integer)
        If input Is Nothing Then Throw New ArgumentNullException(NameOf(input))
        Return MethodAsyncInternal(input)
    End Function

    Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
        ' code that uses await goes here
        Await Task.Delay(1)
        Return value
    End Function
End Class

Dalším případem, kdy je takové delegování užitečné, je implementace optimalizace rychlé cesty a chcete vrátit úlohu uloženou v mezipaměti.

Pracovní zatížení

Asynchronní operace vázané na výpočetní prostředky i vstupně-výstupní operace můžete implementovat jako metody TAP. Když však z knihovny veřejně zpřístupníte metody TAP, poskytněte je jenom pro úlohy, které zahrnují vstupně-výstupní operace. Tyto operace můžou zahrnovat také výpočty, ale neměly by být čistě výpočetní. Pokud je metoda čistě vázaná na výpočetní prostředky, zpřístupňte ji pouze jako synchronní implementaci. Kód, který ho využívá, pak může zvolit, jestli se má zabalit vyvolání této synchronní metody do úlohy, aby bylo možné práci přesměrovat do jiného vlákna nebo dosáhnout paralelismu. Pokud je metoda vázaná na vstupně-výstupní operace, zpřístupňte ji pouze jako asynchronní implementaci.

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

Třída System.Threading.Tasks.Task dobře funguje pro reprezentaci výpočetních operací náročných na výpočetní výkon. Ve výchozím nastavení využívá speciální podporu v rámci ThreadPool třídy k zajištění efektivního provádění. Poskytuje také významnou kontrolu nad tím, kdy, kde a jak se provádějí asynchronní výpočty.

Úlohy vázané na výpočetní výkon generujte 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. Použijte Run pro snadné spuštění výpočetně náročného úkolu, který se zaměřuje na fond vláken. Tato metoda je upřednostňovaným mechanismem spuštění výpočetní úlohy. Používejte StartNew přímo pouze v případech, kdy chcete mít nad úkolem přesnější kontrolu.

  • V .NET Framework 4 použijte metodu TaskFactory.StartNew. Přijímá delegáta (obvykle Action<T> nebo Func<TResult>) pro asynchronní spuštění. Pokud zadáte Action<T> delegáta, vrátí System.Threading.Tasks.Task metoda objekt, který představuje asynchronní spuštění tohoto delegáta. Pokud zadáte delegáta Func<TResult> , metoda vrátí System.Threading.Tasks.Task<TResult> objekt. Přetížení metody StartNew přijímají token zrušení (CancellationToken), možnosti vytvoření úlohy (TaskCreationOptions) a plánovač úloh (TaskScheduler). Tyto parametry poskytují jemně odstupňovanou kontrolu nad plánováním a prováděním úlohy. Instance továrny, která cílí na aktuální plánovač úloh, je k dispozici jako statická vlastnost (Factory) Task třídy. Například: Task.Factory.StartNew(…).

  • Pokud chcete generovat a naplánovat úlohu samostatně, použijte konstruktory Task typu a Start metodu. Veřejné metody musí vracet pouze úkoly, které jsou už spuštěny.

  • Použijte metodu přetížení Task.ContinueWith. Tato metoda vytvoří novou úlohu, která je naplánována po dokončení jiného úkolu. ContinueWith Některé overloady přijímají token zrušení, možnosti pokračování a plánovač úloh pro lepší kontrolu nad plánováním a prováděním navazující úlohy.

  • Použijte metody TaskFactory.ContinueWhenAll a TaskFactory.ContinueWhenAny. Tyto metody vytvoří novou úlohu, která se naplánuje, když se dokončí všechna nebo jakákoli zadaná sada úkolů. Tyto metody také poskytují přetížení pro řízení plánování a provádění těchto úloh.

Ve výpočetních úkolech může systém zabránit spuštění naplánované úlohy, pokud obdrží požadavek na zrušení před spuštěním úkolu. Pokud například zadáte token zrušení (CancellationToken objekt), můžete tento token předat asynchronnímu kódu, který token monitoruje. Token můžete také poskytnout jedné z dříve uvedených metod, jako je StartNew nebo Run, aby Task modul runtime mohl také monitorovat token.

Představte si například asynchronní metodu, která vykreslí obrázek. Tělo úlohy může vyhodnocovat token zrušení, aby se kód ukončil předčasně, pokud během vykreslování dorazí požadavek na zrušení. Kromě toho, pokud žádost o zrušení přijde před zahájením vykreslování, chcete zabránit operaci vykreslování:

internal static 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

Note

Tato ukázka používá Bitmap, což vyžaduje balíček System.Drawing.Common a je podporován pouze na Windows. Model úlohy vázaný na výpočetní výkon – pomocí Task.Run s CancellationToken – platí na všech platformách; nahraďte multiplatformní knihovnu pro práci s obrázky pro cílové platformy, které nejsou Windows.

Výpočetní úlohy končí ve Canceled stavu, pokud platí alespoň jedna z následujících podmínek:

  • Požadavek na zrušení dorazí prostřednictvím objektu CancellationToken, který je poskytován jako argument k metodě vytvoření, například StartNew nebo Run, a to před tím, než úloha přejde do stavu Running.

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

Pokud jiná výjimka zůstane nezpracována v těle úlohy, úloha skončí ve Faulted stavu. Všechny pokusy o čekání na úkol nebo přístup k jeho výsledku způsobí vyvolání výjimky.

Vstupně-výstupní úkoly

Pokud chcete vytvořit úlohu, která by neměla přímo používat vlákno pro celé spuštění, použijte typ TaskCompletionSource<TResult> . Tento typ zveřejňuje Task vlastnost, která vrací přidruženou Task<TResult> instanci. Životní cyklus tohoto úkolu můžete řídit pomocí TaskCompletionSource<TResult> metod, jako jsou SetResult, SetException, SetCanceled a jejich TrySet variant.

Předpokládejme, že chcete vytvořit úkol, který se dokončí po zadaném časovém období. Můžete například chtít zpozdit aktivitu v uživatelském rozhraní. Třída System.Threading.Timer již poskytuje schopnost asynchronně vyvolat delegáta po zadaném časovém období. Pomocí TaskCompletionSource<TResult>, můžete dát Task<TResult> přední na č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

Pro Task.Delay tento účel je k dispozici metoda. Můžete ho 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á obecný protějšek. Task<TResult> Je však odvozen z Task, takže můžete použít obecný TaskCompletionSource<TResult> objekt pro vstupně-výstupní metody, které jednoduše vracejí úlohu. Uděláte to tak, že použijete zdroj s fiktivním TResult (Boolean je dobrá výchozí volba, ale pokud máte obavy o uživatele provádění downcastu Task na Task<TResult> typ, můžete místo toho použít soukromý TResult typ). Například Delay metoda v předchozím příkladu vrátí aktuální čas spolu s výsledným posunem (Task<DateTimeOffset>). Pokud je taková výsledná hodnota nepotřebná, je možné metodu místo toho zakódovat následujícím způsobem (všimněte si změny návratového typu a změny argumentu na TrySetResult):

public static Task<bool> DelaySimple(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 DelaySimple(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 se neomezují pouze na výpočetní operace vázané na vstupně-výstupní operace. Mohou představovat kombinaci těchto dvou. Ve skutečnosti často kombinujete více asynchronních operací do větších smíšených operací. Například RenderAsync metoda v předchozím příkladu provádí výpočetně náročnou operaci vykreslení obrázku na základě určitého vstupu imageData. To imageData může pocházet z webové služby, ke které asynchronně přistupujete:

public static 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

Note

Tato ukázka používá Bitmap, což vyžaduje balíček System.Drawing.Common a je podporován pouze na Windows. Vzorec řetězení asynchronního stahování s asynchronní operací vázanou na výpočetní výkon je uplatnitelný na všech platformách; nahraďte multiplatformní knihovnou pro práci s obrázky pro cíle mimo Windows.

Tento příklad také ukazuje, jak lze jeden token zrušení vytvořit z více asynchronních operací. Další informace naleznete v sekci o využití zrušení v Využívání asynchronního vzoru založeného na úlohách.

Viz také