Implementacja wzorca asynchronicznego opartego na zadaniach

Wzorzec asynchroniczny oparty na zadaniach (TAP) można zaimplementować na trzy sposoby: przy użyciu kompilatorów języka C# i Visual Basic w programie Visual Studio, ręcznie lub za pomocą kombinacji metod kompilatora i metod ręcznych. W poniższych sekcjach szczegółowo omówiono każdą metodę. Możesz użyć wzorca TAP, aby zaimplementować zarówno operacje asynchroniczne powiązane z obliczeniami, jak i operacje asynchroniczne związane z operacjami we/wy. W sekcji Obciążenia omówiono każdy typ operacji.

Generowanie metod TAP

Korzystanie z kompilatorów

Począwszy od programu .NET Framework 4.5, każda metoda przypisana async słowom kluczowym (Async w Visual Basic) jest uznawana za metodę asynchroniczną, a kompilatory języka C# i Visual Basic wykonują niezbędne przekształcenia w celu zaimplementowania metody asynchronicznie przy użyciu interfejsu TAP. Metoda asynchroniczna powinna zwracać System.Threading.Tasks.Task obiekt lub System.Threading.Tasks.Task<TResult> . W przypadku tego ostatniego treść funkcji powinna zwrócić wartość TResult, a kompilator gwarantuje, że ten wynik zostanie udostępniony za pośrednictwem wynikowego obiektu zadania. Podobnie wszystkie wyjątki, które nie sąobsługiwane w treści metody, są ułożone do zadania wyjściowego i powodują zakończenie wynikowego zadania w TaskStatus.Faulted stanie . Wyjątkiem od tej reguły jest OperationCanceledException sytuacja, gdy (lub typ pochodny) nie jest obsługiwane, w takim przypadku wynikowe zadanie kończy się w TaskStatus.Canceled stanie.

Ręczne generowanie metod TAP

Wzorzec tap można zaimplementować ręcznie, aby lepiej kontrolować implementację. Kompilator korzysta z publicznego obszaru powierzchni uwidocznionego z System.Threading.Tasks przestrzeni nazw i typów pomocniczych w System.Runtime.CompilerServices przestrzeni nazw. Aby zaimplementować interfejs TAP samodzielnie, należy utworzyć obiekt, wykonać operację asynchroniczną, a po jej zakończeniu wywołać SetResultmetodę TaskCompletionSource<TResult> , SetExceptionlub SetCanceledTry wersję jednej z tych metod. W przypadku ręcznego implementowania metody TAP należy wykonać wynikowe zadanie po zakończeniu reprezentowanej operacji asynchronicznej. Na przykład:

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

Podejście hybrydowe

Przydatne może być ręczne zaimplementowanie wzorca TAP, ale delegowanie podstawowej logiki implementacji do kompilatora. Na przykład możesz użyć podejścia hybrydowego, jeśli chcesz zweryfikować argumenty poza metodą asynchroniczną wygenerowaną przez System.Threading.Tasks.Task kompilator, aby wyjątki mogły uciec do bezpośredniego obiektu metody, a nie przez obiekt:

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

Innym przypadkiem, w którym takie delegowanie jest przydatne, jest zaimplementowanie optymalizacji szybkiej ścieżki i zwrócenie buforowanego zadania.

Pakiety robocze

Operacje asynchroniczne powiązane z obliczeniami i we/wy można zaimplementować jako metody TAP. Jednak gdy metody TAP są udostępniane publicznie z biblioteki, powinny być udostępniane tylko w przypadku obciążeń obejmujących operacje związane z operacjami we/wy (mogą również obejmować obliczenia, ale nie powinny być wyłącznie obliczeniowe). Jeśli metoda jest wyłącznie powiązana z obliczeniami, powinna być uwidoczniona tylko jako implementacja synchroniczna. Kod, który go używa, może następnie zdecydować, czy opakowować wywołanie tej metody synchronicznej do zadania, aby odciążyć pracę do innego wątku lub osiągnąć równoległość. A jeśli metoda jest powiązana we/wy, powinna być uwidoczniona tylko jako implementacja asynchroniczna.

Zadania związane z obliczeniami

Klasa System.Threading.Tasks.Task doskonale nadaje się do reprezentowania operacji intensywnie korzystających z obliczeń. Domyślnie korzysta ze specjalnej obsługi w ThreadPool klasie w celu zapewnienia wydajnego wykonywania, a także zapewnia znaczącą kontrolę nad tym, kiedy, gdzie i w jaki sposób są wykonywane obliczenia asynchroniczne.

Zadania związane z obliczeniami można wygenerować na następujące sposoby:

  • W programach .NET Framework 4.5 lub nowszych (w tym .NET Core i .NET 5+) użyj metody statycznej Task.Run jako skrótu do TaskFactory.StartNew. Możesz użyć Run polecenia , aby łatwo uruchomić zadanie powiązane z obliczeniami, które jest przeznaczone dla puli wątków. Jest to preferowany mechanizm uruchamiania zadania powiązanego z obliczeniami. Używaj StartNew bezpośrednio tylko wtedy, gdy chcesz uzyskać bardziej szczegółową kontrolę nad zadaniem.

  • W programie .NET Framework 4 użyj TaskFactory.StartNew metody , która akceptuje delegata (zazwyczaj lub Action<T> ) Func<TResult>do wykonania asynchronicznie. Jeśli podasz Action<T> delegata, metoda zwraca System.Threading.Tasks.Task obiekt reprezentujący asynchroniczne wykonanie tego delegata. Jeśli podasz delegata Func<TResult> , metoda zwróci System.Threading.Tasks.Task<TResult> obiekt. Przeciążenia StartNew metody akceptują token anulowania (CancellationToken), opcje tworzenia zadań (TaskCreationOptions) i harmonogram zadań (TaskScheduler), z których wszystkie zapewniają szczegółową kontrolę nad planowaniem i wykonywaniem zadania. Wystąpienie fabryki, które jest przeznaczone dla bieżącego Task harmonogramu zadań, jest dostępne jako właściwość statyczna (Factory) klasy, na przykład: Task.Factory.StartNew(…).

  • Użyj konstruktorów Task typu i Start metody , jeśli chcesz wygenerować i zaplanować zadanie oddzielnie. Metody publiczne muszą zwracać tylko zadania, które zostały już uruchomione.

  • Użyj przeciążeń Task.ContinueWith metody . Ta metoda tworzy nowe zadanie zaplanowane po zakończeniu innego zadania. ContinueWith Niektóre przeciążenia akceptują token anulowania, opcje kontynuacji i harmonogram zadań, aby lepiej kontrolować planowanie i wykonywanie zadania kontynuacji.

  • TaskFactory.ContinueWhenAll Użyj metod i TaskFactory.ContinueWhenAny . Te metody tworzą nowe zadanie, które jest zaplanowane po zakończeniu wszystkich lub dowolnego zestawu dostarczonych zadań. Te metody zapewniają również przeciążenia do kontrolowania planowania i wykonywania tych zadań.

W zadaniach związanych z obliczeniami system może uniemożliwić wykonanie zaplanowanego zadania, jeśli otrzyma żądanie anulowania przed rozpoczęciem uruchamiania zadania. W związku z tym, jeśli podasz token anulowania (CancellationToken obiekt), możesz przekazać ten token do kodu asynchronicznego, który monitoruje token. Możesz również podać token do jednej z wcześniej wymienionych metod, takich jak StartNew lub Run tak, aby Task środowisko uruchomieniowe mogło również monitorować token.

Rozważmy na przykład metodę asynchroniczną, która renderuje obraz. Treść zadania może sondować token anulowania, aby kod mógł zakończyć się wcześnie, jeśli żądanie anulowania zostanie dostarczone podczas renderowania. Ponadto jeśli żądanie anulowania zostanie dostarczone przed rozpoczęciem renderowania, należy zapobiec operacji renderowania:

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

Zadania związane z obliczeniami kończą się stanem Canceled , jeśli spełniony jest co najmniej jeden z następujących warunków:

  • Żądanie anulowania jest dostarczane przez CancellationToken obiekt, który jest udostępniany jako argument metody tworzenia (na przykład StartNew lub Run) przed przejściem zadania do Running stanu.

  • Wyjątek OperationCanceledException jest nieobsługiwany w treści takiego zadania, który zawiera ten sam CancellationToken wyjątek, który jest przekazywany do zadania, i że token pokazuje, że żądanie anulowania jest wymagane.

Jeśli inny wyjątek nie jest używany w treści zadania, zadanie kończy się w Faulted stanie i wszelkie próby oczekiwania na zadanie lub uzyskanie dostępu do jego wyniku powoduje zgłoszenie wyjątku.

Zadania związane z we/wy

Aby utworzyć zadanie, które nie powinno być bezpośrednio wspierane przez wątek dla całego wykonywania, użyj TaskCompletionSource<TResult> typu . Ten typ uwidacznia Task właściwość zwracającą skojarzone Task<TResult> wystąpienie. Cykl życia tego zadania jest kontrolowany przez TaskCompletionSource<TResult> metody, takie jak SetResult, SetException, SetCanceledi ich TrySet warianty.

Załóżmy, że chcesz utworzyć zadanie, które zostanie ukończone po określonym przedziale czasu. Na przykład możesz opóźnić działanie w interfejsie użytkownika. Klasa System.Threading.Timer zapewnia już możliwość asynchronicznego wywoływania delegata po określonym przedziale czasu i przy użyciu TaskCompletionSource<TResult> polecenia można umieścić Task<TResult> front na czasomierzu, na przykład:

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 jest udostępniana w tym celu i można jej użyć wewnątrz innej metody asynchronicznej, na przykład w celu zaimplementowania asynchronicznej pętli sondowania:

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

Klasa TaskCompletionSource<TResult> nie ma ogólnego odpowiednika. Task<TResult> Jednak pochodzi z Taskklasy , aby można było użyć TaskCompletionSource<TResult> ogólnego obiektu dla metod związanych z we/wy, które po prostu zwracają zadanie. W tym celu można użyć źródła z manekinem TResult (Boolean jest dobrym wyborem domyślnym, ale jeśli martwisz się o użytkownika Task downcasting go do elementu Task<TResult>, możesz użyć typu prywatnego TResult zamiast tego). Na przykład Delay metoda w poprzednim przykładzie zwraca bieżący czas wraz z wynikowym przesunięciem (Task<DateTimeOffset>). Jeśli taka wartość wyniku jest niepotrzebna, można zamiast tego kodować metodę w następujący sposób (zwróć uwagę na zmianę typu zwracanego i zmianę 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

Mieszane zadania związane z obliczeniami i związane z we/wy

Metody asynchroniczne nie są ograniczone tylko do operacji związanych z obliczeniami lub operacjami powiązanymi we/wy, ale mogą reprezentować kombinację tych dwóch operacji. W rzeczywistości wiele operacji asynchronicznych jest często łączone w większe operacje mieszane. Na przykład RenderAsync metoda w poprzednim przykładzie wykonała operację intensywnie korzystającą z obliczeń w celu renderowania obrazu na podstawie niektórych danych wejściowych imageData. Może to imageData pochodzić z usługi internetowej, do której asynchronicznie uzyskujesz dostęp:

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

W tym przykładzie pokazano również, jak pojedynczy token anulowania może być wątkowy za pomocą wielu operacji asynchronicznych. Aby uzyskać więcej informacji, zobacz sekcję użycie anulowania w temacie Korzystanie ze wzorca asynchronicznego opartego na zadaniach.

Zobacz też