Przeczytaj w języku angielskim

Udostępnij za pośrednictwem


Łączenie zadań łańcuchowych przy użyciu zadań kontynuacji

W programowaniu asynchronicznym często występuje jedna operacja asynchroniczna, która wywołuje drugą operację po zakończeniu. Kontynuacje umożliwiają wykonywanie operacji potomnych w celu korzystania z wyników pierwszej operacji. Tradycyjnie kontynuacje zostały wykonane przy użyciu metod wywołania zwrotnego. W bibliotece równoległej zadań (TPL) te same funkcje są udostępniane przez zadania kontynuacji. Zadanie kontynuacji (znane również jako kontynuacja) jest asynchronicznym zadaniem wywoływanym przez inne zadanie, znane jako antecedent, po zakończeniu przedniania.

Kontynuacje są stosunkowo łatwe do użycia, ale mimo to są potężne i elastyczne. Można na przykład:

  • Przekaż dane z poprzednich do kontynuacji.
  • Określ dokładne warunki, w których zostanie wywołana kontynuacja lub nie zostanie wywołana.
  • Anuluj kontynuację przed rozpoczęciem lub współpracy, gdy jest uruchomiony.
  • Podaj wskazówki dotyczące sposobu planowania kontynuacji.
  • Wywołaj wiele kontynuacji z tego samego antycedentu.
  • Wywołaj jedną kontynuację po zakończeniu wszystkich lub jednego z wielu przeddentów.
  • Łańcuch kontynuacji jeden po drugim do dowolnej długości.
  • Użyj kontynuacji do obsługi wyjątków zgłaszanych przez antecedent.

Informacje o kontynuacjach

Kontynuacja jest zadaniem utworzonym WaitingForActivation w stanie . Jest on aktywowany automatycznie po zakończeniu zadania lub zadań podrzędnych. Wywołanie Task.Start kontynuacji w kodzie użytkownika zgłasza System.InvalidOperationException wyjątek.

Kontynuacja jest sama i Task nie blokuje wątku, na którym została uruchomiona. Wywołaj metodę Task.Wait w celu zablokowania do momentu zakończenia zadania kontynuacji.

Tworzenie kontynuacji pojedynczego antycedenta

Należy utworzyć kontynuację, która jest wykonywana po zakończeniu Task.ContinueWith jego działania przez wywołanie metody . W poniższym przykładzie przedstawiono podstawowy wzorzec (w celu zapewnienia przejrzystości pominięto obsługę wyjątków). Wykonuje ono zadanie przeddentowe taskA , które zwraca DayOfWeek obiekt, który wskazuje nazwę bieżącego dnia tygodnia. Po taskA zakończeniu obiekt antecedent reprezentuje jego wyniki w metodzie kontynuacji ContinueWith . Wynik zadania przedzibowego jest zapisywany w konsoli programu .

using System;
using System.Threading.Tasks;

public class SimpleExample
{
    public static async Task Main()
    {
        // Declare, assign, and start the antecedent task.
        Task<DayOfWeek> taskA = Task.Run(() => DateTime.Today.DayOfWeek);

        // Execute the continuation when the antecedent finishes.
        await taskA.ContinueWith(antecedent => Console.WriteLine($"Today is {antecedent.Result}."));
    }
}
// The example displays the following output:
//       Today is Monday.

Tworzenie kontynuacji dla wielu przeddentów

Możesz również utworzyć kontynuację, która będzie uruchamiana po zakończeniu dowolnej lub całej grupy zadań. Aby wykonać kontynuację po zakończeniu wszystkich poprzednich zadań, można wywołać metodę static (Shared w Visual Basic) Task.WhenAll lub metodę wystąpienia TaskFactory.ContinueWhenAll . Aby wykonać kontynuację po zakończeniu któregokolwiek z poprzednich zadań, można wywołać metodę static (Shared w Visual Basic) Task.WhenAny lub metodę wystąpienia TaskFactory.ContinueWhenAny .

Wywołania funkcji Task.WhenAll i Task.WhenAny przeciążenia nie blokują wątku wywołującego. Jednak zazwyczaj wywołujesz wszystkie Task.WhenAll(IEnumerable<Task>) metody i Task.WhenAll(Task[]) , aby pobrać zwróconą Task<TResult>.Result właściwość, która blokuje wątek wywołujący.

Poniższy przykład wywołuje metodę Task.WhenAll(IEnumerable<Task>) , aby utworzyć zadanie kontynuacji, które odzwierciedla wyniki 10 poprzednich zadań. Każde zadanie poprzedza kwadraty wartości indeksu, która waha się od jednego do 10. Jeśli antecedents zakończą się pomyślnie (ich Task.Status właściwość to TaskStatus.RanToCompletion), Task<TResult>.Result właściwość kontynuacji jest tablicą Task<TResult>.Result wartości zwracanych przez każdy antecedent. W przykładzie dodano je do obliczenia sumy kwadratów dla wszystkich liczb z zakresu od jednego do 10:

using System.Collections.Generic;
using System;
using System.Threading.Tasks;

public class WhenAllExample
{
    public static async Task Main()
    {
        var tasks = new List<Task<int>>();
        for (int ctr = 1; ctr <= 10; ctr++)
        {
            int baseValue = ctr;
            tasks.Add(Task.Factory.StartNew(b => (int)b! * (int)b, baseValue));
        }

        var results = await Task.WhenAll(tasks);

        int sum = 0;
        for (int ctr = 0; ctr <= results.Length - 1; ctr++)
        {
            var result = results[ctr];
            Console.Write($"{result} {((ctr == results.Length - 1) ? "=" : "+")} ");
            sum += result;
        }

        Console.WriteLine(sum);
    }
}
// The example displays the similar output:
//    1 + 4 + 9 + 16 + 25 + 36 + 49 + 64 + 81 + 100 = 385

Opcje kontynuacji

Podczas tworzenia kontynuacji pojedynczego zadania można użyć ContinueWith przeciążenia, które przyjmuje System.Threading.Tasks.TaskContinuationOptions wartość wyliczenia, aby określić warunki, w których rozpoczyna się kontynuacja. Można na przykład określić, że kontynuacja ma być uruchamiana tylko wtedy, gdy antecedent zakończy się pomyślnie lub tylko wtedy, gdy zakończy się w stanie uszkodzonym. Jeśli warunek nie jest spełniony, gdy antecedent jest gotowy do wywołania kontynuacji, kontynuacja przechodzi bezpośrednio do TaskStatus.Canceled stanu i nie można go uruchomić później.

Wiele metod kontynuacji wielu zadań, takich jak przeciążenia TaskFactory.ContinueWhenAll metody, zawiera System.Threading.Tasks.TaskContinuationOptions również parametr. Jednak tylko podzbiór wszystkich System.Threading.Tasks.TaskContinuationOptions elementów członkowskich wyliczenia jest prawidłowy. Możesz określić System.Threading.Tasks.TaskContinuationOptions wartości, które mają odpowiedniki w wyliczenie System.Threading.Tasks.TaskCreationOptions , takie jak TaskContinuationOptions.AttachedToParent, TaskContinuationOptions.LongRunningi TaskContinuationOptions.PreferFairness. Jeśli określisz dowolną z NotOn opcji lub OnlyOn z kontynuacją wielu zadań, ArgumentOutOfRangeException w czasie wykonywania zostanie zgłoszony wyjątek.

Aby uzyskać więcej informacji na temat opcji kontynuacji zadań, zobacz TaskContinuationOptions artykuł.

Przekazywanie danych do kontynuacji

Metoda Task.ContinueWith przekazuje odwołanie do antecedent jako argument delegata użytkownika kontynuacji. Jeśli antecedent jest obiektem System.Threading.Tasks.Task<TResult> , a zadanie zostało uruchomione do momentu jego ukończenia, kontynuacja może uzyskać dostęp do Task<TResult>.Result właściwości zadania.

Właściwość Task<TResult>.Result blokuje się do momentu ukończenia zadania. Jeśli jednak zadanie zostało anulowane lub zostało uszkodzone, próba uzyskania dostępu do Result właściwości zgłasza AggregateException wyjątek. Możesz uniknąć tego problemu OnlyOnRanToCompletion , używając opcji , jak pokazano w poniższym przykładzie:

using System;
using System.Threading.Tasks;

public class ResultExample
{
    public static async Task Main()
    {
       var task = Task.Run(
           () =>
           {
                DateTime date = DateTime.Now;
                return date.Hour > 17
                    ? "evening"
                    : date.Hour > 12
                        ? "afternoon"
                        : "morning";
            });
        
        await task.ContinueWith(
            antecedent =>
            {
                Console.WriteLine($"Good {antecedent.Result}!");
                Console.WriteLine($"And how are you this fine {antecedent.Result}?");
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
   }
}
// The example displays the similar output:
//       Good afternoon!
//       And how are you this fine afternoon?

Jeśli chcesz, aby kontynuacja przebiegła nawet wtedy, gdy poprzednia operacja nie przebiegła do pomyślnego ukończenia, musisz chronić się przed wyjątkiem. Jedną z metod jest przetestowanie Task.Status właściwości antecedent i próba uzyskania dostępu do Result właściwości tylko wtedy, gdy stan nie Faulted jest lub Canceled. Możesz również zbadać Exception właściwość antecedent. Aby uzyskać więcej informacji, zobacz Obsługa wyjątków. Poniższy przykład modyfikuje poprzedni przykład, aby uzyskać dostęp do właściwości antecedent tylko Task<TResult>.Result wtedy, gdy jego stan to TaskStatus.RanToCompletion:

using System;
using System.Threading.Tasks;

public class ResultTwoExample
{
    public static async Task Main() =>
        await Task.Run(
            () =>
            {
                DateTime date = DateTime.Now;
                return date.Hour > 17
                   ? "evening"
                   : date.Hour > 12
                       ? "afternoon"
                       : "morning";
            })
            .ContinueWith(
                antecedent =>
                {
                    if (antecedent.Status == TaskStatus.RanToCompletion)
                    {
                        Console.WriteLine($"Good {antecedent.Result}!");
                        Console.WriteLine($"And how are you this fine {antecedent.Result}?");
                    }
                    else if (antecedent.Status == TaskStatus.Faulted)
                    {
                        Console.WriteLine(antecedent.Exception!.GetBaseException().Message);
                    }
                });
}
// The example displays output like the following:
//       Good afternoon!
//       And how are you this fine afternoon?

Anulowanie kontynuacji

Właściwość Task.Status kontynuacji jest ustawiona na TaskStatus.Canceled w następujących sytuacjach:

Jeśli zadanie i jego kontynuacja reprezentują dwie części tej samej operacji logicznej, możesz przekazać ten sam token anulowania do obu zadań, jak pokazano w poniższym przykładzie. Składa się z antecedent, który generuje listę liczb całkowitych, które są podzielne przez 33, które przekazuje do kontynuacji. Kontynuacja z kolei wyświetla listę. Zarówno antecedent, jak i kontynuacja są regularnie wstrzymywane dla losowych interwałów. Ponadto System.Threading.Timer obiekt jest używany do wykonywania Elapsed metody po pięciosekundowym interwale limitu czasu. W tym przykładzie wywoływana jest CancellationTokenSource.Cancel metoda, która powoduje, że aktualnie wykonywane zadanie wywołuje metodę CancellationToken.ThrowIfCancellationRequested . Niezależnie od tego CancellationTokenSource.Cancel , czy metoda jest wywoływana, gdy antecedent lub jego kontynuacja jest wykonywane, zależy od czasu trwania losowych wstrzymań. Jeśli antecedent zostanie anulowany, kontynuacja nie zostanie uruchomiona. Jeśli poprzednio nie anulowano, token może być nadal używany do anulowania kontynuacji.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public class CancellationExample
{
    static readonly Random s_random = new Random((int)DateTime.Now.Ticks);

    public static async Task Main()
    {
        using var cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;
        var timer = new Timer(Elapsed, cts, 5000, Timeout.Infinite);

        var task = Task.Run(
            async () =>
            {
                var product33 = new List<int>();
                for (int index = 1; index < short.MaxValue; index++)
                {
                    if (token.IsCancellationRequested)
                    {
                        Console.WriteLine("\nCancellation requested in antecedent...\n");
                        token.ThrowIfCancellationRequested();
                    }
                    if (index % 2000 == 0)
                    {
                        int delay = s_random.Next(16, 501);
                        await Task.Delay(delay);
                    }
                    if (index % 33 == 0)
                    {
                        product33.Add(index);
                    }
                }

                return product33.ToArray();
            }, token);

        Task<double> continuation = task.ContinueWith(
            async antecedent =>
            {
                Console.WriteLine("Multiples of 33:\n");
                int[] array = antecedent.Result;
                for (int index = 0; index < array.Length; index++)
                {
                    if (token.IsCancellationRequested)
                    {
                        Console.WriteLine("\nCancellation requested in continuation...\n");
                        token.ThrowIfCancellationRequested();
                    }
                    if (index % 100 == 0)
                    {
                        int delay = s_random.Next(16, 251);
                        await Task.Delay(delay);
                    }

                    Console.Write($"{array[index]:N0}{(index != array.Length - 1 ? ", " : "")}");

                    if (Console.CursorLeft >= 74)
                    {
                        Console.WriteLine();
                    }
                }
                Console.WriteLine();
                return array.Average();
            }, token).Unwrap();

        try
        {
            await task;
            double result = await continuation;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        Console.WriteLine("\nAntecedent Status: {0}", task.Status);
        Console.WriteLine("Continuation Status: {0}", continuation.Status);
    }

    static void Elapsed(object? state)
    {
        if (state is CancellationTokenSource cts)
        {
            cts.Cancel();
            Console.WriteLine("\nCancellation request issued...\n");
        }
    }
}
// The example displays the similar output:
//     Multiples of 33:
//     
//     33, 66, 99, 132, 165, 198, 231, 264, 297, 330, 363, 396, 429, 462, 495, 528,
//     561, 594, 627, 660, 693, 726, 759, 792, 825, 858, 891, 924, 957, 990, 1,023,
//     1,056, 1,089, 1,122, 1,155, 1,188, 1,221, 1,254, 1,287, 1,320, 1,353, 1,386,
//     1,419, 1,452, 1,485, 1,518, 1,551, 1,584, 1,617, 1,650, 1,683, 1,716, 1,749,
//     1,782, 1,815, 1,848, 1,881, 1,914, 1,947, 1,980, 2,013, 2,046, 2,079, 2,112,
//     2,145, 2,178, 2,211, 2,244, 2,277, 2,310, 2,343, 2,376, 2,409, 2,442, 2,475,
//     2,508, 2,541, 2,574, 2,607, 2,640, 2,673, 2,706, 2,739, 2,772, 2,805, 2,838,
//     2,871, 2,904, 2,937, 2,970, 3,003, 3,036, 3,069, 3,102, 3,135, 3,168, 3,201,
//     3,234, 3,267, 3,300, 3,333, 3,366, 3,399, 3,432, 3,465, 3,498, 3,531, 3,564,
//     3,597, 3,630, 3,663, 3,696, 3,729, 3,762, 3,795, 3,828, 3,861, 3,894, 3,927,
//     3,960, 3,993, 4,026, 4,059, 4,092, 4,125, 4,158, 4,191, 4,224, 4,257, 4,290,
//     4,323, 4,356, 4,389, 4,422, 4,455, 4,488, 4,521, 4,554, 4,587, 4,620, 4,653,
//     4,686, 4,719, 4,752, 4,785, 4,818, 4,851, 4,884, 4,917, 4,950, 4,983, 5,016,
//     5,049, 5,082, 5,115, 5,148, 5,181, 5,214, 5,247, 5,280, 5,313, 5,346, 5,379,
//     5,412, 5,445, 5,478, 5,511, 5,544, 5,577, 5,610, 5,643, 5,676, 5,709, 5,742,
//     Cancellation request issued...
//
//     5,775,
//     Cancellation requested in continuation...
//       
//     The operation was canceled.
//       
//     Antecedent Status: RanToCompletion
//     Continuation Status: Canceled

Możesz również uniemożliwić kontynuowanie wykonywania, jeśli jego przeddent zostanie anulowany bez podawania kontynuacji tokenu anulowania. Podaj token, określając TaskContinuationOptions.NotOnCanceled opcję podczas tworzenia kontynuacji, jak pokazano w poniższym przykładzie:

using System;
using System.Threading;
using System.Threading.Tasks;

public class CancellationTwoExample
{
    public static async Task Main()
    {
        using var cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;
        cts.Cancel();

        var task = Task.FromCanceled(token);
        Task continuation =
            task.ContinueWith(
                antecedent => Console.WriteLine("The continuation is running."),
                TaskContinuationOptions.NotOnCanceled);

        try
        {
            await task;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
            Console.WriteLine();
        }

        Console.WriteLine($"Task {task.Id}: {task.Status:G}");
        Console.WriteLine($"Task {continuation.Id}: {continuation.Status:G}");
    }
}
// The example displays the similar output:
//       TaskCanceledException: A task was canceled.
//
//       Task 1: Canceled
//       Task 2: Canceled

Po przejściu Canceled kontynuacji do stanu może to mieć wpływ na kontynuacje, w zależności od TaskContinuationOptions tego, które zostały określone dla tych kontynuacji.

Kontynuacje, które są usuwane, nie zostaną uruchomione.

Kontynuacje i zadania podrzędne

Kontynuacja nie jest uruchamiana, dopóki nie zostanie ukończona poprzednia, a wszystkie dołączone zadania podrzędne zostaną ukończone. Kontynuacja nie czeka na zakończenie odłączonych zadań podrzędnych. W poniższych dwóch przykładach pokazano podrzędne zadania, które są dołączone do i odłączone od przedzidentu, który tworzy kontynuację. W poniższym przykładzie kontynuacja jest uruchamiana tylko po zakończeniu wszystkich zadań podrzędnych, a wiele uruchomień przykładu generuje identyczne dane wyjściowe za każdym razem. W przykładzie jest uruchamiany przeddent przez wywołanie TaskFactory.StartNew metody , ponieważ domyślnie Task.Run metoda tworzy zadanie nadrzędne, którego domyślną opcją tworzenia zadania jest TaskCreationOptions.DenyChildAttach.

using System;
using System.Threading.Tasks;

public class AttachedExample
{
    public static async Task Main()
    {
        await Task.Factory
                  .StartNew(
            () =>
            {
                Console.WriteLine($"Running antecedent task {Task.CurrentId}...");
                Console.WriteLine("Launching attached child tasks...");
                for (int ctr = 1; ctr <= 5; ctr++)
                {
                    int index = ctr;
                    Task.Factory.StartNew(async value =>
                    {
                        Console.WriteLine($"   Attached child task #{value} running");
                        await Task.Delay(1000);
                    }, index, TaskCreationOptions.AttachedToParent);
                }
                Console.WriteLine("Finished launching attached child tasks...");
            }).ContinueWith(
                antecedent =>
                    Console.WriteLine($"Executing continuation of Task {antecedent.Id}"));
    }
}
// The example displays the similar output:
//     Running antecedent task 1...
//     Launching attached child tasks...
//     Finished launching attached child tasks...
//        Attached child task #1 running
//        Attached child task #5 running
//        Attached child task #3 running
//        Attached child task #2 running
//        Attached child task #4 running
//     Executing continuation of Task 1

Jeśli zadania podrzędne są odłączone od antecedent, kontynuacja jest uruchamiana zaraz po zakończeniu przedzidentu, niezależnie od stanu zadań podrzędnych. W związku z tym wiele uruchomień poniższego przykładu może wygenerować zmienne dane wyjściowe, które zależą od sposobu obsługi każdego zadania podrzędnego przez harmonogram zadań:

using System;
using System.Threading.Tasks;

public class DetachedExample
{
    public static async Task Main()
    {
        Task task =
            Task.Factory.StartNew(
                () =>
                {
                    Console.WriteLine($"Running antecedent task {Task.CurrentId}...");
                    Console.WriteLine("Launching attached child tasks...");
                    for (int ctr = 1; ctr <= 5; ctr++)
                    {
                        int index = ctr;
                        Task.Factory.StartNew(
                            async value =>
                            {
                                Console.WriteLine($"   Attached child task #{value} running");
                                await Task.Delay(1000);
                            }, index);
                    }
                    Console.WriteLine("Finished launching detached child tasks...");
                }, TaskCreationOptions.DenyChildAttach);

        Task continuation =
            task.ContinueWith(
                antecedent =>
                    Console.WriteLine($"Executing continuation of Task {antecedent.Id}"));

        await continuation;

        Console.ReadLine();
    }
}
// The example displays the similar output:
//     Running antecedent task 1...
//     Launching attached child tasks...
//     Finished launching detached child tasks...
//     Executing continuation of Task 1
//        Attached child task #1 running
//        Attached child task #5 running
//        Attached child task #2 running
//        Attached child task #3 running
//        Attached child task #4 running

Stan końcowy zadania podrzędnego jest zależny od stanu końcowego wszystkich dołączonych zadań podrzędnych. Stan odłączonych zadań podrzędnych nie ma wpływu na element nadrzędny. Aby uzyskać więcej informacji, zobacz Dołączone i odłączone zadania podrzędne.

Kojarzenie stanu z kontynuacjami

Możesz skojarzyć dowolny stan z kontynuacją zadania. Metoda ContinueWith udostępnia przeciążone wersje, które przyjmują wartość reprezentującą Object stan kontynuacji. Później możesz uzyskać dostęp do tego obiektu stanu przy użyciu Task.AsyncState właściwości . Ten obiekt stanu jest, null jeśli nie podasz wartości.

Stan kontynuacji jest przydatny podczas konwertowania istniejącego kodu, który używa asynchronicznego modelu programowania (APM) do korzystania z TPL. W APM można podać stan obiektu w metodzie BeginMethod , a później można użyć IAsyncResult.AsyncState właściwości w celu uzyskania dostępu do tego stanu. Aby zachować ten stan podczas konwertowania kodu, który używa APM do korzystania z TPL, należy użyć ContinueWith metody .

Stan kontynuacji może być również przydatny podczas pracy z obiektami Task w debugerze programu Visual Studio. Na przykład w oknie Zadania równoległe kolumna Zadanie wyświetla ciąg reprezentujący obiekt stanu dla każdego zadania. Aby uzyskać więcej informacji na temat okna Zadań równoległych, zobacz Korzystanie z okna Zadania.

W poniższym przykładzie pokazano, jak używać stanu kontynuacji. Tworzy łańcuch zadań kontynuacji. Każde zadanie udostępnia bieżący czas, DateTime obiekt, dla state parametru ContinueWith metody . Każdy DateTime obiekt reprezentuje czas utworzenia zadania kontynuacji. Każde zadanie generuje w wyniku drugi DateTime obiekt reprezentujący czas zakończenia zadania. Po zakończeniu wszystkich zadań w tym przykładzie zostanie wyświetlony czas utworzenia i godzina zakończenia każdego zadania kontynuacji.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

class ContinuationStateExample
{
    static DateTime DoWork()
    {
        Thread.Sleep(2000);

        return DateTime.Now;
    }

    static async Task Main()
    {
        Task<DateTime> task = Task.Run(() => DoWork());

        var continuations = new List<Task<DateTime>>();
        for (int i = 0; i < 5; i++)
        {
            task = task.ContinueWith((antecedent, _) => DoWork(), DateTime.Now);
            continuations.Add(task);
        }

        await task;

        foreach (Task<DateTime> continuation in continuations)
        {
            DateTime start = (DateTime)continuation.AsyncState!;
            DateTime end = continuation.Result;

            Console.WriteLine($"Task was created at {start.TimeOfDay} and finished at {end.TimeOfDay}.");
        }

        Console.ReadLine();
    }
}
// The example displays the similar output:
//     Task was created at 10:56:21.1561762 and finished at 10:56:25.1672062.
//     Task was created at 10:56:21.1610677 and finished at 10:56:27.1707646.
//     Task was created at 10:56:21.1610677 and finished at 10:56:29.1743230.
//     Task was created at 10:56:21.1610677 and finished at 10:56:31.1779883.
//     Task was created at 10:56:21.1610677 and finished at 10:56:33.1837083.

Kontynuacje zwracające typy zadań

Czasami może być konieczne utworzenie łańcucha kontynuacji zwracającej Task typ. Te zadania są określane jako zadania zagnieżdżone. Gdy zadanie nadrzędne wywołuje Task<TResult>.ContinueWith element i udostępnia continuationFunction zwracane zadanie, można wywołać Unwrap metodę , aby utworzyć zadanie serwera proxy reprezentujące operację <Task<Task<T>>> asynchroniczną elementu lub Task(Of Task(Of T)) (Visual Basic).

W poniższym przykładzie pokazano, jak używać kontynuacji, które opakowujące dodatkowe zadania zwracają funkcje. Każdą kontynuację można rozpasać, ujawniając zadanie wewnętrzne, które zostało opakowane.

using System;
using System.Threading;
using System.Threading.Tasks;

public class UnwrapExample
{
    public static async Task Main()
    {
        Task<int> taskOne = RemoteIncrement(0);
        Console.WriteLine("Started RemoteIncrement(0)");

        Task<int> taskTwo = RemoteIncrement(4)
            .ContinueWith(t => RemoteIncrement(t.Result))
            .Unwrap().ContinueWith(t => RemoteIncrement(t.Result))
            .Unwrap().ContinueWith(t => RemoteIncrement(t.Result))
            .Unwrap();

        Console.WriteLine("Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)");

        try
        {
            await taskOne;
            Console.WriteLine("Finished RemoteIncrement(0)");

            await taskTwo;
            Console.WriteLine("Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)");
        }
        catch (Exception e)
        {
            Console.WriteLine($"A task has thrown the following (unexpected) exception:\n{e}");
        }
    }

    static Task<int> RemoteIncrement(int number) =>
        Task<int>.Factory.StartNew(
            obj =>
            {
                Thread.Sleep(1000);

                int x = (int)(obj!);
                Console.WriteLine("Thread={0}, Next={1}", Thread.CurrentThread.ManagedThreadId, ++x);
                return x;
            },
            number);
}

// The example displays the similar output:
//     Started RemoteIncrement(0)
//     Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)
//     Thread=4, Next=1
//     Finished RemoteIncrement(0)
//     Thread=5, Next=5
//     Thread=6, Next=6
//     Thread=6, Next=7
//     Thread=6, Next=8
//     Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)

Aby uzyskać więcej informacji na temat korzystania z programu Unwrap, zobacz Jak: odpakowywanie zagnieżdżonego zadania.

Obsługa wyjątków zgłaszanych z kontynuacji

Relacja antecedent-kontynuacja nie jest relacją nadrzędny-podrzędny. Wyjątki zgłaszane przez kontynuacje nie są propagowane do antecedent. W związku z tym należy obsługiwać wyjątki zgłaszane przez kontynuacje, tak jak można je obsłużyć w innym zadaniu w następujący sposób:

  • Możesz użyć Waitmetody , WaitAll, lub WaitAny jej ogólnego odpowiednika, aby zaczekać na kontynuację. Możesz poczekać na antycedent i jego kontynuacje w tej samej try instrukcji, jak pokazano w poniższym przykładzie:
using System;
using System.Threading.Tasks;

public class ExceptionExample
{
    public static async Task Main()
    {
        Task<int> task = Task.Run(
            () =>
            {
                Console.WriteLine($"Executing task {Task.CurrentId}");
                return 54;
            });

        var continuation = task.ContinueWith(
            antecedent =>
            {
                Console.WriteLine($"Executing continuation task {Task.CurrentId}");
                Console.WriteLine($"Value from antecedent: {antecedent.Result}");

                throw new InvalidOperationException();
            });

        try
        {
            await task;
            await continuation;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}
// The example displays the similar output:
//       Executing task 1
//       Executing continuation task 2
//       Value from antecedent: 54
//       Operation is not valid due to the current state of the object.
  • Możesz użyć drugiej kontynuacji, aby obserwować Exception właściwość pierwszej kontynuacji. W poniższym przykładzie zadanie próbuje odczytać z nieistniejącego pliku. Kontynuacja wyświetla następnie informacje o wyjątku w zadaniu antecedent.
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

public class ExceptionTwoExample
{
    public static async Task Main()
    {
        var task = Task.Run(
            () =>
            {
                string fileText = File.ReadAllText(@"C:\NonexistentFile.txt");
                return fileText;
            });

        Task continuation = task.ContinueWith(
            antecedent =>
            {
                var fileNotFound =
                    antecedent.Exception
                        ?.InnerExceptions
                        ?.FirstOrDefault(e => e is FileNotFoundException) as FileNotFoundException;

                if (fileNotFound != null)
                {
                    Console.WriteLine(fileNotFound.Message);
                }
            }, TaskContinuationOptions.OnlyOnFaulted);

        await continuation;

        Console.ReadLine();
    }
}
// The example displays the following output:
//        Could not find file 'C:\NonexistentFile.txt'.

Ponieważ była uruchamiana z opcją TaskContinuationOptions.OnlyOnFaulted , kontynuacja jest wykonywana tylko wtedy, gdy wystąpi wyjątek w przedniezieniu. W związku z tym można założyć, że właściwość antecedenta Exception nie nulljest . Jeśli kontynuacja wykonuje, czy wyjątek jest zgłaszany w antecedent, musi sprawdzić, czy właściwość antecedent Exception nie null jest przed podjęciem próby obsługi wyjątku, jak pokazuje następujący fragment kodu:

var fileNotFound =
    antecedent.Exception
        ?.InnerExceptions
        ?.FirstOrDefault(e => e is FileNotFoundException) as FileNotFoundException;

if (fileNotFound != null)
{
    Console.WriteLine(fileNotFound.Message);
}

Aby uzyskać więcej informacji, zobacz Obsługa wyjątków.

  • Jeśli kontynuacja jest dołączonym zadaniem podrzędnym, które zostało utworzone przy użyciu TaskContinuationOptions.AttachedToParent opcji, jego wyjątki będą propagowane przez element nadrzędny z powrotem do wątku wywołującego, tak jak w przypadku każdego innego dołączonego elementu podrzędnego. Aby uzyskać więcej informacji, zobacz Dołączone i odłączone zadania podrzędne.

Zobacz też