Chaînage de tâches à l’aide de tâches de continuation

En programmation asynchrone, il est courant pour une opération asynchrone d’appeler une deuxième opération une fois qu’elle est terminée. Les continuations permettent aux opérations descendantes de consommer les résultats de la première opération. Habituellement, les continuations se faisaient à l’aide de méthodes de rappel. Dans la bibliothèque parallèle de tâches (TPL), les mêmes fonctionnalités sont fournies par les tâches de continuation. Une tâche de continuation (également appelée tout simplement continuation) est une tâche asynchrone appelée par une autre tâche, désignée comme antécédent, quand ce dernier est terminé.

Les continuations sont relativement faciles à utiliser, tout en étant puissantes et flexibles. Par exemple, vous pouvez :

  • passer des données de l'antécédent à la continuation ;
  • spécifier les conditions précises sous lesquelles la continuation doit être ou non appelée ;
  • annuler une continuation avant qu’elle ne soit lancée ou pendant son exécution de manière coopérative ;
  • fournir des conseils sur la manière de planifier la continuation ;
  • appeler plusieurs continuations depuis le même antécédent ;
  • appeler une continuation quand certains ou tous les antécédents se terminent ;
  • chaîner des continuations les unes à la suite des autres à une longueur arbitraire ;
  • utiliser une continuation pour gérer des exceptions levées par l'antécédent.

À propos des continuations

Une continuation est une tâche qui est créée à l’état WaitingForActivation. Elle s’active automatiquement à la fin de la tâche ou des tâches de son antécédent. Appeler Task.Start sur une continuation dans le code utilisateur lève une exception System.InvalidOperationException .

Une continuation est un Task et ne bloque pas le thread sur lequel elle a démarré. Appelez la méthode Task.Wait pour bloquer jusqu'à ce que la tâche de continuation se termine.

Créer une continuation pour un seul antécédent

Vous créez une continuation qui s'exécute quand son antécédent est terminé en appelant la méthode Task.ContinueWith . L’exemple suivant montre le modèle de base (pour plus de clarté, la gestion des exceptions a été omise). Il exécute une tâche d’antécédent, taskA, qui retourne un objet DayOfWeek indiquant le jour actuel de la semaine. Une fois taskA terminée, antecedent représente ses résultats dans la méthode de continuation ContinueWith. Le résultat de la tâche d’antécédent est écrit dans la console.

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.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        ' Execute the antecedent.
        Dim taskA As Task(Of DayOfWeek) = Task.Run(Function() DateTime.Today.DayOfWeek)

        ' Execute the continuation when the antecedent finishes.
        Dim continuation As Task = taskA.ContinueWith(Sub(antecedent)
                                                          Console.WriteLine("Today is {0}.", antecedent.Result)
                                                      End Sub)
        continuation.Wait()
    End Sub
End Module
' The example displays output like the following output:
'       Today is Monday.

Créer une continuation pour plusieurs antécédents

Vous pouvez également créer une continuation qui va s’exécuter une fois qu’une tâche ou un groupe de tâches sont terminés. Pour exécuter une continuation quand toutes les tâches d’antécédent sont terminées, vous pouvez appeler la méthode statique Task.WhenAll (Shared en Visual Basic) ou la méthode TaskFactory.ContinueWhenAll d’instance. Pour exécuter une continuation quand l’une des tâches d’antécédent est terminée, vous pouvez appeler la méthode statique Task.WhenAny (Shared en Visual Basic) ou la méthode TaskFactory.ContinueWhenAny d’instance.

Les appels aux surcharges Task.WhenAll et Task.WhenAny ne bloquent pas le thread appelant. Cependant, vous appelez généralement toutes les méthodes, sauf Task.WhenAll(IEnumerable<Task>) et Task.WhenAll(Task[]), pour récupérer la propriété Task<TResult>.Result retournée, ce qui bloque le thread appelant.

L’exemple suivant appelle la méthode Task.WhenAll(IEnumerable<Task>) pour créer une tâche de continuation qui reflète les résultats de ses dix tâches d’antécédent. Chaque tâche d’antécédent élève au carré une valeur d’index allant de 1 à 10. Si les antécédents se terminent correctement (leur propriété Task.Status vaut TaskStatus.RanToCompletion), la propriété Task<TResult>.Result de la continuation est un tableau de valeurs Task<TResult>.Result retournées par chaque antécédent. L’exemple les ajoute pour calculer la somme des carrés de tous les nombres compris entre 1 et 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
Imports System.Collections.Generic
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim tasks As New List(Of Task(Of Integer))()
        For ctr As Integer = 1 To 10
            Dim baseValue As Integer = ctr
            tasks.Add(Task.Factory.StartNew(Function(b)
                                                Dim i As Integer = CInt(b)
                                                Return i * i
                                            End Function, baseValue))
        Next
        Dim continuation = Task.WhenAll(tasks)

        Dim sum As Long = 0
        For ctr As Integer = 0 To continuation.Result.Length - 1
            Console.Write("{0} {1} ", continuation.Result(ctr),
                          If(ctr = continuation.Result.Length - 1, "=", "+"))
            sum += continuation.Result(ctr)
        Next
        Console.WriteLine(sum)
    End Sub
End Module
' The example displays the following output:
'       1 + 4 + 9 + 16 + 25 + 36 + 49 + 64 + 81 + 100 = 385

Options de continuation

Quand vous créez une continuation de tâche unique, vous pouvez utiliser une surcharge ContinueWith qui prend une valeur d'énumération System.Threading.Tasks.TaskContinuationOptions pour spécifier les conditions sous lesquelles la continuation doit se lancer. Par exemple, vous pouvez spécifier que la continuation doit être exécutée uniquement si l‘antécédent se termine correctement ou uniquement s‘il se termine avec un état d‘erreur. Si cette condition n’est pas remplie quand l’antécédent est prêt à appeler la continuation, la continuation passe directement à l’état TaskStatus.Canceled et ne peut plus être lancée ultérieurement.

De nombreuses méthodes de continuation multitâche, telles que les surcharges de la méthode TaskFactory.ContinueWhenAll, incluent également un paramètre System.Threading.Tasks.TaskContinuationOptions. Néanmoins, seul un sous-ensemble des membres de l’énumération System.Threading.Tasks.TaskContinuationOptions est valide. Vous pouvez spécifier des valeurs System.Threading.Tasks.TaskContinuationOptions qui ont des équivalents dans l'énumération System.Threading.Tasks.TaskCreationOptions , telles que TaskContinuationOptions.AttachedToParent, TaskContinuationOptions.LongRunninget TaskContinuationOptions.PreferFairness. Si vous spécifiez n'importe laquelle des options NotOn ou OnlyOn avec une continuation multitâche, une exception ArgumentOutOfRangeException est levée au moment de l'exécution.

Pour plus d’informations sur les options de continuation de tâche, consultez l’article TaskContinuationOptions.

Passer des données à une continuation

La méthode Task.ContinueWith passe une référence à l’antécédent en tant qu’argument au délégué d’utilisateur de la continuation. Si l'antécédent est un objet System.Threading.Tasks.Task<TResult> et que la tâche s'est exécutée normalement jusqu'à son terme, la continuation peut accéder à la propriété Task<TResult>.Result de la tâche.

La propriété Task<TResult>.Result se bloque jusqu'à ce que la tâche soit terminée. Toutefois, si la tâche a été annulée ou a rencontré une erreur, toute tentative d'accéder à la propriété Result lève une exception AggregateException . Vous pouvez éviter ce problème à l’aide de l’option OnlyOnRanToCompletion, comme indiqué dans l’exemple suivant :

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?
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim t = Task.Run(Function()
                             Dim dat As DateTime = DateTime.Now
                             If dat = DateTime.MinValue Then
                                 Throw New ArgumentException("The clock is not working.")
                             End If

                             If dat.Hour > 17 Then
                                 Return "evening"
                             Else If dat.Hour > 12 Then
                                 Return "afternoon"
                             Else
                                 Return "morning"
                             End If
                         End Function)
        Dim c = t.ContinueWith(Sub(antecedent)
                                   Console.WriteLine("Good {0}!",
                                                     antecedent.Result)
                                   Console.WriteLine("And how are you this fine {0}?",
                                                     antecedent.Result)
                               End Sub, TaskContinuationOptions.OnlyOnRanToCompletion)
        c.Wait()
    End Sub
End Module
' The example displays output like the following:
'       Good afternoon!
'       And how are you this fine afternoon?

Si vous voulez que la continuation s’exécute même si l’exécution de l’antécédent ne s’est pas correctement terminée, vous devez vous attendre à des exceptions. Une approche consiste à tester la propriété Task.Status de l’antécédent et à essayer d’accéder à la propriété Result uniquement si l’état n’est pas Faulted ou Canceled. Vous pouvez également examiner la propriété Exception de l'antécédent. Pour plus d’informations, consultez l’article Gestion des exceptions. L’exemple suivant modifie l’exemple précédent de manière à accéder à la propriété Task<TResult>.Result de l’antécédent uniquement si son état est 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?
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim t = Task.Run(Function()
                             Dim dat As DateTime = DateTime.Now
                             If dat = DateTime.MinValue Then
                                 Throw New ArgumentException("The clock is not working.")
                             End If

                             If dat.Hour > 17 Then
                                 Return "evening"
                             Else If dat.Hour > 12 Then
                                 Return "afternoon"
                             Else
                                 Return "morning"
                             End If
                         End Function)
        Dim c = t.ContinueWith(Sub(antecedent)
                                   If t.Status = TaskStatus.RanToCompletion Then
                                       Console.WriteLine("Good {0}!",
                                                         antecedent.Result)
                                       Console.WriteLine("And how are you this fine {0}?",
                                                         antecedent.Result)
                                   Else If t.Status = TaskStatus.Faulted Then
                                       Console.WriteLine(t.Exception.GetBaseException().Message)
                                   End If
                               End Sub)
    End Sub
End Module
' The example displays output like the following:
'       Good afternoon!
'       And how are you this fine afternoon?

Annuler une continuation

La propriété Task.Status d'une continuation est définie sur TaskStatus.Canceled dans les situations suivantes :

Si une tâche et sa continuation représentent deux parties de la même opération logique, vous pouvez passer le même jeton d’annulation aux deux tâches, comme illustré dans l’exemple suivant. Ce dernier comprend un antécédent qui génère une liste d'entiers divisibles par 33, transmise ensuite à la continuation. À son tour, la continuation affiche la liste. L‘antécédent et la continuation marquent régulièrement des pauses de durée aléatoire. En outre, un objet System.Threading.Timer est utilisé pour exécuter la méthode Elapsed après un délai de cinq secondes. Cet exemple appelle la méthode CancellationTokenSource.Cancel, ce qui amène la tâche en cours d’exécution à appeler la méthode CancellationToken.ThrowIfCancellationRequested. La durée des pauses générées de manière aléatoire détermine si la méthode CancellationTokenSource.Cancel est appelée pendant l'exécution de l'antécédent ou de sa continuation. Si l’antécédent est annulé, la continuation ne démarre pas. Si l’antécédent n’est pas annulé, le jeton peut toujours être utilisé pour annuler la continuation.

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
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim rnd As New Random()
        Dim lockObj As New Object()
        Dim cts As New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token
        Dim timer As New Timer(AddressOf Elapsed, cts, 5000, Timeout.Infinite)

        Dim t = Task.Run(Function()
                             Dim product33 As New List(Of Integer)()
                             For ctr As Integer = 1 To Int16.MaxValue
                                 ' Check for cancellation.
                                 If token.IsCancellationRequested Then
                                     Console.WriteLine("\nCancellation requested in antecedent...\n")
                                     token.ThrowIfCancellationRequested()
                                 End If
                                 ' Introduce a delay.
                                 If ctr Mod 2000 = 0 Then
                                     Dim delay As Integer
                                     SyncLock lockObj
                                         delay = rnd.Next(16, 501)
                                     End SyncLock
                                     Thread.Sleep(delay)
                                 End If

                                 ' Determine if this is a multiple of 33.
                                 If ctr Mod 33 = 0 Then product33.Add(ctr)
                             Next
                             Return product33.ToArray()
                         End Function, token)

        Dim continuation = t.ContinueWith(Sub(antecedent)
                                              Console.WriteLine("Multiples of 33:" + vbCrLf)
                                              Dim arr = antecedent.Result
                                              For ctr As Integer = 0 To arr.Length - 1
                                                  If token.IsCancellationRequested Then
                                                      Console.WriteLine("{0}Cancellation requested in continuation...{0}",
                                                                        vbCrLf)
                                                      token.ThrowIfCancellationRequested()
                                                  End If

                                                  If ctr Mod 100 = 0 Then
                                                      Dim delay As Integer
                                                      SyncLock lockObj
                                                          delay = rnd.Next(16, 251)
                                                      End SyncLock
                                                      Thread.Sleep(delay)
                                                  End If
                                                  Console.Write("{0:N0}{1}", arr(ctr),
                                                                If(ctr <> arr.Length - 1, ", ", ""))
                                                  If Console.CursorLeft >= 74 Then Console.WriteLine()
                                              Next
                                              Console.WriteLine()
                                          End Sub, token)

        Try
            continuation.Wait()
        Catch e As AggregateException
            For Each ie In e.InnerExceptions
                Console.WriteLine("{0}: {1}", ie.GetType().Name,
                                  ie.Message)
            Next
        Finally
            cts.Dispose()
        End Try

        Console.WriteLine(vbCrLf + "Antecedent Status: {0}", t.Status)
        Console.WriteLine("Continuation Status: {0}", continuation.Status)
    End Sub

    Private Sub Elapsed(state As Object)
        Dim cts As CancellationTokenSource = TryCast(state, CancellationTokenSource)
        If cts Is Nothing Then return

        cts.Cancel()
        Console.WriteLine("{0}Cancellation request issued...{0}", vbCrLf)
    End Sub
End Module
' The example displays output like the following:
'    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,
'    5,775, 5,808, 5,841, 5,874, 5,907, 5,940, 5,973, 6,006, 6,039, 6,072, 6,105,
'    6,138, 6,171, 6,204, 6,237, 6,270, 6,303, 6,336, 6,369, 6,402, 6,435, 6,468,
'    6,501, 6,534, 6,567, 6,600, 6,633, 6,666, 6,699, 6,732, 6,765, 6,798, 6,831,
'    6,864, 6,897, 6,930, 6,963, 6,996, 7,029, 7,062, 7,095, 7,128, 7,161, 7,194,
'    7,227, 7,260, 7,293, 7,326, 7,359, 7,392, 7,425, 7,458, 7,491, 7,524, 7,557,
'    7,590, 7,623, 7,656, 7,689, 7,722, 7,755, 7,788, 7,821, 7,854, 7,887, 7,920,
'    7,953, 7,986, 8,019, 8,052, 8,085, 8,118, 8,151, 8,184, 8,217, 8,250, 8,283,
'    8,316, 8,349, 8,382, 8,415, 8,448, 8,481, 8,514, 8,547, 8,580, 8,613, 8,646,
'    8,679, 8,712, 8,745, 8,778, 8,811, 8,844, 8,877, 8,910, 8,943, 8,976, 9,009,
'    9,042, 9,075, 9,108, 9,141, 9,174, 9,207, 9,240, 9,273, 9,306, 9,339, 9,372,
'    9,405, 9,438, 9,471, 9,504, 9,537, 9,570, 9,603, 9,636, 9,669, 9,702, 9,735,
'    9,768, 9,801, 9,834, 9,867, 9,900, 9,933, 9,966, 9,999, 10,032, 10,065, 10,098,
'    10,131, 10,164, 10,197, 10,230, 10,263, 10,296, 10,329, 10,362, 10,395, 10,428,
'    10,461, 10,494, 10,527, 10,560, 10,593, 10,626, 10,659, 10,692, 10,725, 10,758,
'    10,791, 10,824, 10,857, 10,890, 10,923, 10,956, 10,989, 11,022, 11,055, 11,088,
'    11,121, 11,154, 11,187, 11,220, 11,253, 11,286, 11,319, 11,352, 11,385, 11,418,
'    11,451, 11,484, 11,517, 11,550, 11,583, 11,616, 11,649, 11,682, 11,715, 11,748,
'    11,781, 11,814, 11,847, 11,880, 11,913, 11,946, 11,979, 12,012, 12,045, 12,078,
'    12,111, 12,144, 12,177, 12,210, 12,243, 12,276, 12,309, 12,342, 12,375, 12,408,
'    12,441, 12,474, 12,507, 12,540, 12,573, 12,606, 12,639, 12,672, 12,705, 12,738,
'    12,771, 12,804, 12,837, 12,870, 12,903, 12,936, 12,969, 13,002, 13,035, 13,068,
'    13,101, 13,134, 13,167, 13,200, 13,233, 13,266,
'    Cancellation requested in continuation...
'
'
'    Cancellation request issued...
'
'    TaskCanceledException: A task was canceled.
'
'    Antecedent Status: RanToCompletion
'    Continuation Status: Canceled

Vous pouvez également empêcher l’exécution d’une continuation si son antécédent est annulé sans fournir de jeton d’annulation à la continuation. Fournissez le jeton en spécifiant l’option TaskContinuationOptions.NotOnCanceled lorsque vous créez la continuation, comme illustré dans l’exemple suivant :

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
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim cts As New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token
        cts.Cancel()

        Dim t As Task = Task.FromCanceled(token)
        Dim continuation As Task = t.ContinueWith(Sub(antecedent)
                                                      Console.WriteLine("The continuation is running.")
                                                  End Sub, TaskContinuationOptions.NotOnCanceled)
        Try
            t.Wait()
        Catch e As AggregateException
            For Each ie In e.InnerExceptions
                Console.WriteLine("{0}: {1}", ie.GetType().Name, ie.Message)
            Next
            Console.WriteLine()
        Finally
            cts.Dispose()
        End Try

        Console.WriteLine("Task {0}: {1:G}", t.Id, t.Status)
        Console.WriteLine("Task {0}: {1:G}", continuation.Id,
                          continuation.Status)
    End Sub
End Module
' The example displays the following output:
'       TaskCanceledException: A task was canceled.
'
'       Task 1: Canceled
'       Task 2: Canceled

Une fois que la continuation passe à l’état Canceled, elle peut affecter les continuations qui suivent, selon les TaskContinuationOptions spécifiées pour ces continuations.

Les continuations supprimées ne sont pas lancées.

Continuations et tâches enfants

Une continuation ne s’exécute pas tant que l’antécédent et toutes ses tâches enfants attachées ne sont pas terminés. Une continuation n’attend pas la fin des tâches enfants détachées. Les deux exemples suivants illustrent des tâches enfants attachées à un antécédent qui crée une continuation, et détachées de celui-ci. Dans l’exemple suivant, la continuation s’exécute uniquement quand toutes les tâches enfants sont terminées. L’exécution de l’exemple à plusieurs reprise génère systématiquement la même sortie. L’exemple lance l’antécédent en appelant la méthode TaskFactory.StartNew, car, par défaut, la méthode Task.Run crée une tâche parent dont l’option de création de tâche par défaut est 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
Imports System.Threading
Imports System.Threading.Tasks

Public Module Example
    Public Sub Main()
        Dim t = Task.Factory.StartNew(Sub()
                                          Console.WriteLine("Running antecedent task {0}...",
                                                            Task.CurrentId)
                                          Console.WriteLine("Launching attached child tasks...")
                                          For ctr As Integer = 1 To 5
                                              Dim index As Integer = ctr
                                              Task.Factory.StartNew(Sub(value)
                                                                        Console.WriteLine("   Attached child task #{0} running",
                                                                                          value)
                                                                        Thread.Sleep(1000)
                                                                    End Sub, index, TaskCreationOptions.AttachedToParent)
                                          Next
                                          Console.WriteLine("Finished launching attached child tasks...")
                                      End Sub)
        Dim continuation = t.ContinueWith(Sub(antecedent)
                                              Console.WriteLine("Executing continuation of Task {0}",
                                                                antecedent.Id)
                                          End Sub)
        continuation.Wait()
    End Sub
End Module
' The example displays the following output:
'       Running antecedent task 1...
'       Launching attached child tasks...
'       Finished launching attached child tasks...
'          Attached child task #5 running
'          Attached child task #1 running
'          Attached child task #2 running
'          Attached child task #3 running
'          Attached child task #4 running
'       Executing continuation of Task 1

Toutefois, si des tâches enfants sont détachées de l'antécédent, la continuation s'exécute dès que l'antécédent a terminé, indépendamment de l'état des tâches enfants. Ainsi, différentes exécutions de l’exemple suivant peuvent générer différentes sorties en fonction de la façon dont le Planificateur de tâches a géré chaque tâche enfant :

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
Imports System.Threading
Imports System.Threading.Tasks

Public Module Example
    Public Sub Main()
        Dim t = Task.Factory.StartNew(Sub()
                                          Console.WriteLine("Running antecedent task {0}...",
                                                            Task.CurrentId)
                                          Console.WriteLine("Launching attached child tasks...")
                                          For ctr As Integer = 1 To 5
                                              Dim index As Integer = ctr
                                              Task.Factory.StartNew(Sub(value)
                                                                        Console.WriteLine("   Attached child task #{0} running",
                                                                                          value)
                                                                        Thread.Sleep(1000)
                                                                    End Sub, index)
                                          Next
                                          Console.WriteLine("Finished launching detached child tasks...")
                                      End Sub, TaskCreationOptions.DenyChildAttach)
        Dim continuation = t.ContinueWith(Sub(antecedent)
                                              Console.WriteLine("Executing continuation of Task {0}",
                                                                antecedent.Id)
                                          End Sub)
        continuation.Wait()
    End Sub
End Module
' The example displays output like the following:
'       Running antecedent task 1...
'       Launching attached child tasks...
'       Finished launching detached child tasks...
'          Attached child task #1 running
'          Attached child task #2 running
'          Attached child task #5 running
'          Attached child task #3 running
'       Executing continuation of Task 1
'          Attached child task #4 running

L’état final de la tâche d’antécédent dépend de l’état final des tâches enfants attachées. L’état des tâches enfants détachées n’affecte pas le parent. Pour plus d'informations, consultez Tâches enfants attachées et détachées.

Associer un état à des continuations

Vous pouvez associer un état arbitraire à une continuation de tâche. La méthode ContinueWith fournit des versions surchargées prenant chacune une valeur Object qui représente l'état de la continuation. Vous pouvez ensuite accéder à cet objet d'état à l'aide de la propriété Task.AsyncState . Cet objet d’état est null si vous ne fournissez pas de valeur.

L'état de continuation est utile quand vous convertissez du code existant qui recourt au modèle de programmation asynchrone (APM) pour utiliser la bibliothèque parallèle de tâches. Dans l’APM, vous pouvez fournir l’état de l’objet dans la méthode BeginMethod, puis utiliser la propriété IAsyncResult.AsyncState pour accéder à cet état. Pour conserver cet état quand vous convertissez du code qui recourt à l’APM pour utiliser la bibliothèque parallèle de tâches, vous utilisez la méthode ContinueWith.

L'état de continuation peut également être utile si vous employez des objets Task dans le débogueur Visual Studio. Par exemple, dans la fenêtre Tâches parallèles , la colonne Tâche affiche la représentation sous forme de chaîne de l'objet d'état pour chaque tâche. Pour plus d’informations sur la fenêtre Tâches parallèles, consultez Utilisation de la fenêtre Tâches.

L'exemple suivant montre comment utiliser l'état de continuation. Il crée une chaîne de tâches de continuation. Chaque tâche fournit l'heure actuelle, sous la forme d'un objet DateTime , pour le paramètre state de la méthode ContinueWith . Chaque objet DateTime représente l'heure de création de la tâche de continuation. Chaque tâche génère un deuxième objet DateTime qui représente l'heure à laquelle la tâche se termine. Une fois toutes les tâches terminées, cet exemple affiche les heures de création et de fin de chaque tâche de continuation.

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.
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

' Demonstrates how to associate state with task continuations.
Public Module ContinuationState
    ' Simulates a lengthy operation and returns the time at which
    ' the operation completed.
    Public Function DoWork() As Date
        ' Simulate work by suspending the current thread
        ' for two seconds.
        Thread.Sleep(2000)

        ' Return the current time.
        Return Date.Now
    End Function

    Public Sub Main()
        ' Start a root task that performs work.
        Dim t As Task(Of Date) = Task(Of Date).Run(Function() DoWork())

        ' Create a chain of continuation tasks, where each task is
        ' followed by another task that performs work.
        Dim continuations As New List(Of Task(Of DateTime))()
        For i As Integer = 0 To 4
            ' Provide the current time as the state of the continuation.
            t = t.ContinueWith(Function(antecedent, state) DoWork(), DateTime.Now)
            continuations.Add(t)
        Next

        ' Wait for the last task in the chain to complete.
        t.Wait()

        ' Display the creation time of each continuation (the state object)
        ' and the completion time (the result of that task) to the console.
        For Each continuation In continuations
            Dim start As DateTime = CDate(continuation.AsyncState)
            Dim [end] As DateTime = continuation.Result

            Console.WriteLine("Task was created at {0} and finished at {1}.",
               start.TimeOfDay, [end].TimeOfDay)
        Next
    End Sub
End Module
' The example displays output like the following:
'       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.

Continuations qui retournent des types de tâches

Parfois, vous aurez peut-être besoin de chaîner une continuation qui retourne un type Task. Ces tâches sont appelées des tâches imbriquées. Quand une tâche parent appelle Task<TResult>.ContinueWith et fournit une continuationFunction qui retourne une tâche, vous pouvez appeler Unwrap pour créer une tâche proxy qui représente l’opération asynchrone de <Task<Task<T>>> ou Task(Of Task(Of T)) (Visual Basic).

L’exemple suivant montre comment utiliser des continuations qui incluent dans un wrapper des fonctions supplémentaires qui retournent des tâches. Chaque continuation peut être déwrappée, ce qui expose la tâche interne qui a été incluse dans un wrapper.

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))...)
Imports System.Threading

Module UnwrapExample
    Sub Main()
        Dim taskOne As Task(Of Integer) = RemoteIncrement(0)
        Console.WriteLine("Started RemoteIncrement(0)")

        Dim taskTwo As Task(Of Integer) = RemoteIncrement(4).
            ContinueWith(Function(t) RemoteIncrement(t.Result)).
            Unwrap().ContinueWith(Function(t) RemoteIncrement(t.Result)).
            Unwrap().ContinueWith(Function(t) RemoteIncrement(t.Result)).
            Unwrap()

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

        Try
            taskOne.Wait()
            Console.WriteLine("Finished RemoteIncrement(0)")

            taskTwo.Wait()
            Console.WriteLine("Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)")
        Catch e As AggregateException
            Console.WriteLine($"A task has thrown the following (unexpected) exception:{vbLf}{e}")
        End Try
    End Sub

    Function RemoteIncrement(ByVal number As Integer) As Task(Of Integer)
        Return Task(Of Integer).Factory.StartNew(
            Function(obj)
                Thread.Sleep(1000)

                Dim x As Integer = CInt(obj)
                Console.WriteLine("Thread={0}, Next={1}", Thread.CurrentThread.ManagedThreadId, Interlocked.Increment(x))
                Return x
            End Function, number)
    End Function
End Module

' 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))...)

Pour plus d’informations sur l’utilisation de Unwrap, consultez Guide pratique pour déwrapper une tâche imbriquée.

Gérer les exceptions levées par des continuations

Une relation antécédent-continuation n’est pas une relation parent-enfant. Les exceptions levées par les continuations ne sont pas propagées à l’antécédent. Vous pouvez ainsi gérer les exceptions levées par les continuations de la même manière qu’avec une autre tâche, comme suit :

  • Vous pouvez utiliser la méthode Wait, WaitAll, ou WaitAny , ou son équivalent générique, pour attendre la continuation. Vous pouvez attendre un antécédent et ses continuations dans la même instruction try, comme indiqué dans l’exemple suivant :
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.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task(Of Integer).Run(Function()
                                             Console.WriteLine("Executing task {0}",
                                                               Task.CurrentId)
                                             Return 54
                                         End Function)
        Dim continuation = task1.ContinueWith(Sub(antecedent)
                                                  Console.WriteLine("Executing continuation task {0}",
                                                                    Task.CurrentId)
                                                  Console.WriteLine("Value from antecedent: {0}",
                                                                    antecedent.Result)
                                                  Throw New InvalidOperationException()
                                              End Sub)

        Try
            task1.Wait()
            continuation.Wait()
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine(ex.Message)
            Next
        End Try
    End Sub
End Module
' The example displays the following output:
'       Executing task 1
'       Executing continuation task 2
'       Value from antecedent: 54
'       Operation is not valid due to the current state of the object.
  • Vous pouvez utiliser une deuxième continuation pour observer la propriété Exception de la première continuation. Dans l’exemple suivant, une tâche tente de lire un fichier inexistant. La continuation affiche ensuite des informations sur l'exception dans la tâche d'antécédent.
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'.
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim t = Task.Run(Function()
                             Dim s As String = File.ReadAllText("C:\NonexistentFile.txt")
                             Return s
                         End Function)

        Dim c = t.ContinueWith(Sub(antecedent)
                                   ' Get the antecedent's exception information.
                                   For Each ex In antecedent.Exception.InnerExceptions
                                       If TypeOf ex Is FileNotFoundException
                                           Console.WriteLine(ex.Message)
                                       End If
                                   Next
                               End Sub, TaskContinuationOptions.OnlyOnFaulted)

        c.Wait()
    End Sub
End Module
' The example displays the following output:
'       Could not find file 'C:\NonexistentFile.txt'.

Étant donné qu’elle a été exécutée avec l’option TaskContinuationOptions.OnlyOnFaulted, la continuation s’exécute uniquement si une exception se produit dans l’antécédent. Ainsi, elle peut partir du principe que la propriété Exception de l’antécédent n’est pas null. Si la continuation s’exécute indépendamment du fait qu’une exception soit levée ou non dans l’antécédent, elle doit vérifier si la propriété Exception de l’antécédent n’est pas définie sur null avant de gérer l’exception, comme le montre le fragment de code suivant :

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

if (fileNotFound != null)
{
    Console.WriteLine(fileNotFound.Message);
}
' Determine whether an exception occurred.
If antecedent.Exception IsNot Nothing Then
    ' Get the antecedent's exception information.
    For Each ex In antecedent.Exception.InnerExceptions
        If TypeOf ex Is FileNotFoundException
            Console.WriteLine(ex.Message)
        End If
    Next
End If

Pour plus d’informations, consultez l’article Gestion des exceptions.

Voir aussi