Sdílet prostřednictvím


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ů jazyka C# a Visual Basic v sadě 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 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 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.

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:

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

Může být užitečné implementovat vzor TAP ručně, ale delegovat základní logiku pro implementaci 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 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 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. Pokud jsou však metody TAP veřejně zpřístupněny z knihovny, měly by být poskytovány pouze pro úlohy, které zahrnují vstupně-výstupní operace (mohou také zahrnovat 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, 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 ideální 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í a také poskytuje významnou kontrolu nad tím, kdy, kde a jak asynchronní výpočty se provádějí.

Ú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. Můžete použít Run ke snadnému spuštění úlohy vázané na výpočetní prostředky, která cílí 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 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), což vše poskytuje detailní kontrolu nad plánováním a prováděním úlohy. Tovární instance, která cílí na aktuální plánovač úloh, 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 musí vracet pouze úlohy, které už byly 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 modul runtime Task mohl token také monitorovat.

Představte si například asynchronní metodu, která vykreslí obrázek. Tělo úlohy se může dotazovat na token zrušení, aby se kód mohl brzy ukončit, pokud během vykreslování přijde žádost o zrušení. Pokud navíc požadavek na zrušení přijde před zahájení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í ú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.

  • Neošetřená výjimka se vyskytne v těle takového úkolu, přičemž výjimka obsahuje stejný token, který byl předán úkolu, a tento token ukazuje, že byla požadována jeho zrušení.

Pokud v těle úkolu dojde k neošetřené jiné výjimce, úkol skončí ve Faulted stavu a 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

Chcete-li vytvořit úlohu, která by neměla být po celé své spuštění přímo zálohována vláknem, použijte TaskCompletionSource<TResult> typ. Tento typ zveřejňuje Task vlastnost, která vrací přidruženou Task<TResult> instanci. Životní cyklus tohoto úkolu je řízen metodami, jako jsou TaskCompletionSource<TResult>, SetResult, SetException, SetCanceled a jejich TrySet varianty.

Řekněme, ž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 možnost asynchronně vyvolat delegáta po zadaném časovém úseku, a pomocí TaskCompletionSource<TResult> můžete na časovač umístit frontu Task<TResult>, 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á 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. K tomu můžete použít zdroj s fiktivním TResult (Boolean je dobrou výchozí volbou, ale pokud máte obavy z toho, že uživatel převede typ Task na Task<TResult>, můžete místo toho použít soukromý typ TResult). 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> 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 se neomezují pouze na operace vázané na výpočetní výkon nebo vstupně-výstupní operace, ale mohou představovat kombinaci těchto dvou metod. Ve skutečnosti se více asynchronních operací často kombinuje do větších smíšených operací. Například RenderAsync metoda v předchozím příkladu prováděla 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 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é ukazuje, jak může být jeden rušící token propojen prostřednictvím 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é