Tâches enfants attachées et détachées

Une tâche enfant (ou tâche imbriquée) est une instance System.Threading.Tasks.Task créée dans le délégué utilisateur d’une autre tâche, appelée tâche parent. Une tâche enfant peut être détachée ou attachée. Une tâche enfant détachée est une tâche qui s’exécute indépendamment de son parent. Une tâche enfant attachée est une tâche imbriquée créée avec l’option TaskCreationOptions.AttachedToParent dont le parent ne l’empêche pas explicitement ou par défaut d’être attachée. Une tâche peut créer autant de tâches enfants attachées et détachées que le permettent les ressources système.

Le tableau suivant répertorie les principales différences entre les deux types de tâches enfants.

Category Tâches enfants détachées Tâches enfants attachées
Le parent attend que les tâches enfants soient terminées. Non Oui
Le parent propage les exceptions levées par les tâches enfants. Non Oui
Le statut du parent dépend du statut de l'enfant. Non Oui

Dans la plupart des scénarios, nous vous recommandons d’utiliser des tâches enfants détachées, car leurs relations avec les autres tâches sont moins complexes. C'est pourquoi les tâches créées à l'intérieur de tâches parentes sont détachées par défaut et vous devez spécifier explicitement l'option TaskCreationOptions.AttachedToParent pour créer une tâche enfant attachée.

Tâches enfants détachées

Même si une tâche enfant est créée par une tâche parente, par défaut, elle est indépendante de celle-ci. Dans l'exemple suivant, une tâche parente crée une tâche enfant simple. Si vous exécutez l'exemple de code plusieurs fois, vous pouvez remarquer que la sortie de l'exemple diffère de celle indiquée et éventuellement d'une exécution à l'autre. En effet, la tâche parente et les tâches enfants s'exécutent indépendamment les unes des autres ; l'enfant est une tâche détachée. L'exemple attend seulement que la tâche parente se termine, et la tâche enfant ne peut pas s'exécuter ou s'achever avant la fin de l'application console.

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

public class Example
{
   public static void Main()
   {
      var parent = Task.Factory.StartNew(() => {
         Console.WriteLine("Outer task executing.");

         var child = Task.Factory.StartNew(() => {
            Console.WriteLine("Nested task starting.");
            Thread.SpinWait(500000);
            Console.WriteLine("Nested task completing.");
         });
      });

      parent.Wait();
      Console.WriteLine("Outer has completed.");
   }
}
// The example produces output like the following:
//        Outer task executing.
//        Nested task starting.
//        Outer has completed.
//        Nested task completing.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim parent = Task.Factory.StartNew(Sub()
                                               Console.WriteLine("Outer task executing.")
                                               Dim child = Task.Factory.StartNew(Sub()
                                                                                     Console.WriteLine("Nested task starting.")
                                                                                     Thread.SpinWait(500000)
                                                                                     Console.WriteLine("Nested task completing.")
                                                                                 End Sub)
                                           End Sub)
        parent.Wait()
        Console.WriteLine("Outer task has completed.")
    End Sub
End Module
' The example produces output like the following:
'   Outer task executing.
'   Nested task starting.
'   Outer task has completed.
'   Nested task completing.

Si la tâche enfant est représentée par un objet Task<TResult> plutôt que par un objet Task, vous pouvez vous assurer que la tâche parente attend la fin de la tâche enfant en accédant à la propriété Task<TResult>.Result de celle-ci, même s'il s'agit d'une tâche enfant détachée. La propriété Result bloque jusqu’à ce que sa tâche se termine, comme le montre l’exemple suivant.

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

class Example
{
   static void Main()
   {
      var outer = Task<int>.Factory.StartNew(() => {
            Console.WriteLine("Outer task executing.");

            var nested = Task<int>.Factory.StartNew(() => {
                  Console.WriteLine("Nested task starting.");
                  Thread.SpinWait(5000000);
                  Console.WriteLine("Nested task completing.");
                  return 42;
            });

            // Parent will wait for this detached child.
            return nested.Result;
      });

      Console.WriteLine("Outer has returned {0}.", outer.Result);
   }
}
// The example displays the following output:
//       Outer task executing.
//       Nested task starting.
//       Nested task completing.
//       Outer has returned 42.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim parent = Task(Of Integer).Factory.StartNew(Function()
                                                           Console.WriteLine("Outer task executing.")
                                                           Dim child = Task(Of Integer).Factory.StartNew(Function()
                                                                                                             Console.WriteLine("Nested task starting.")
                                                                                                             Thread.SpinWait(5000000)
                                                                                                             Console.WriteLine("Nested task completing.")
                                                                                                             Return 42
                                                                                                         End Function)
                                                           Return child.Result


                                                       End Function)
        Console.WriteLine("Outer has returned {0}", parent.Result)
    End Sub
End Module
' The example displays the following output:
'       Outer task executing.
'       Nested task starting.
'       Detached task completing.
'       Outer has returned 42

Tâches enfants attachées

Contrairement aux tâches enfants détachées, les tâches enfants attachées sont étroitement synchronisées avec le parent. Vous pouvez convertir la tâche enfant détachée dans l'exemple précédent en tâche enfant attachée à l'aide de l'option TaskCreationOptions.AttachedToParent dans l'instruction de création de tâche, comme illustré dans l'exemple suivant. Dans ce code, la tâche enfant attachée se termine avant son parent. La sortie de l'exemple est donc la même chaque fois que vous exécutez le code.

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

public class Example
{
   public static void Main()
   {
      var parent = Task.Factory.StartNew(() => {
            Console.WriteLine("Parent task executing.");
            var child = Task.Factory.StartNew(() => {
                  Console.WriteLine("Attached child starting.");
                  Thread.SpinWait(5000000);
                  Console.WriteLine("Attached child completing.");
            }, TaskCreationOptions.AttachedToParent);
      });
      parent.Wait();
      Console.WriteLine("Parent has completed.");
   }
}
// The example displays the following output:
//       Parent task executing.
//       Attached child starting.
//       Attached child completing.
//       Parent has completed.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim parent = Task.Factory.StartNew(Sub()
                                               Console.WriteLine("Parent task executing")
                                               Dim child = Task.Factory.StartNew(Sub()
                                                                                     Console.WriteLine("Attached child starting.")
                                                                                     Thread.SpinWait(5000000)
                                                                                     Console.WriteLine("Attached child completing.")
                                                                                 End Sub, TaskCreationOptions.AttachedToParent)
                                           End Sub)
        parent.Wait()
        Console.WriteLine("Parent has completed.")
    End Sub
End Module
' The example displays the following output:
'       Parent task executing.
'       Attached child starting.
'       Attached child completing.
'       Parent has completed.

Vous pouvez utiliser des tâches enfants attachées pour créer des graphiques étroitement synchronisés d’opérations asynchrones.

Toutefois, une tâche enfant ne peut s’attacher à son parent que si celui-ci n’interdit pas les tâches enfants attachées. Pour empêcher explicitement l’attachement de tâches enfants à une tâche parente, vous devez spécifier l’option TaskCreationOptions.DenyChildAttach dans le constructeur de classe de la tâche parente ou la méthode TaskFactory.StartNew. Pour une interdiction implicite, vous devez créer les tâches parentes en appelant la méthode Task.Run. L'exemple suivant illustre ce comportement. Il est identique à l’exemple précédent, même si la tâche parente est créée en appelant la méthode Task.Run(Action) plutôt que la méthode TaskFactory.StartNew(Action). Étant donné que la tâche enfant n’est pas en mesure de s’attacher à son parent, la sortie de l’exemple est imprévisible. Étant donné que les options de création de tâches par défaut pour les surcharges Task.Run incluent TaskCreationOptions.DenyChildAttach, cet exemple est fonctionnellement équivalent au premier exemple dans la section « Tâches enfants détachées ».

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

public class Example
{
   public static void Main()
   {
      var parent = Task.Run(() => {
            Console.WriteLine("Parent task executing.");
            var child = Task.Factory.StartNew(() => {
                  Console.WriteLine("Attached child starting.");
                  Thread.SpinWait(5000000);
                  Console.WriteLine("Attached child completing.");
            }, TaskCreationOptions.AttachedToParent);
      });
      parent.Wait();
      Console.WriteLine("Parent has completed.");
   }
}
// The example displays output like the following:
//       Parent task executing.
//       Parent has completed.
//       Attached child starting.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim parent = Task.Run(Sub()
                                  Console.WriteLine("Parent task executing.")
                                  Dim child = Task.Factory.StartNew(Sub()
                                                                        Console.WriteLine("Attached child starting.")
                                                                        Thread.SpinWait(5000000)
                                                                        Console.WriteLine("Attached child completing.")
                                                                    End Sub, TaskCreationOptions.AttachedToParent)
                              End Sub)
        parent.Wait()
        Console.WriteLine("Parent has completed.")
    End Sub
End Module
' The example displays output like the following:
'       Parent task executing.
'       Parent has completed.
'       Attached child starting.

Exceptions dans les tâches enfants

Si une tâche enfant détachée lève une exception, celle-ci doit être observée ou gérée directement dans la tâche parente, comme dans le cas de n’importe quelle tâche non imbriquée. Si une tâche enfant attachée lève une exception, celle-ci est automatiquement propagée vers la tâche parente, puis vers le thread qui attend ou essaie d’accéder à la propriété Task<TResult>.Result de la tâche. Ainsi, en utilisant des tâches enfants attachées, vous pouvez gérer toutes les exceptions en un seul point dans l’appel à Task.Wait sur le thread appelant. Pour plus d’informations, consultez l’article Gestion des exceptions.

Annulation et tâches enfants

L'annulation de tâche est coopérative. Autrement dit, pour être annulable, chaque tâche enfant attachée ou détachée doit surveiller l'état du jeton d'annulation. Pour annuler un parent et tous ses enfants à l'aide d'une demande d'annulation, vous passez le même jeton en tant qu'argument à toutes les tâches et fournissez dans chaque tâche la logique pour répondre à la demande. Pour plus d’informations, consultez Annulation de tâches et Comment : annuler une tâche et ses enfants.

Annulation d'un parent

Si un parent s’annule avant le démarrage de sa tâche enfant, celle-ci ne démarre jamais. Si un parent s’annule une fois que sa tâche enfant a démarré, celle-ci s’exécute jusqu’à son terme sauf si elle possède sa propre logique d’annulation. Pour plus d’informations, voir Annulation de tâches.

Annulation d’une tâche enfant détachée

Si une tâche enfant détachée s’annule à l’aide du jeton passé au parent et que celui-ci n’attend pas la tâche enfant, aucune exception n’est propagée, car l’exception est traitée comme une annulation de coopération bénigne. Ce comportement est identique à celui de toute tâche de niveau supérieur.

Annulation d’une tâche enfant attachée

Quand une tâche enfant attachée s’annule à l’aide du jeton passé à sa tâche parente, une TaskCanceledException est propagée vers le thread intermédiaire à l’intérieur d’une AggregateException. Vous devez attendre la tâche parente pour pouvoir gérer toutes les exceptions bénignes, en plus de toute exception d’erreur propagée vers un graphique de tâches enfants attachées.

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

Empêcher qu'une tâche enfant ne s'attache à son parent

Une exception non gérée levée par une tâche enfant est propagée vers la tâche parente. Vous pouvez vous baser sur ce comportement pour observer toutes les exceptions de tâche enfant à partir d'une seule tâche racine au lieu de parcourir une arborescence de tâches. Toutefois, la propagation d’exception peut être problématique quand une tâche parente n’attend pas d’attachement de la part d’un autre code. Par exemple, imaginez une application qui appelle un composant de bibliothèque tierce à partir d'un objet Task. Si ce composant crée également un objet Task et spécifie TaskCreationOptions.AttachedToParent pour l’attacher à la tâche parente, les exceptions non gérées qui se produisent dans la tâche enfant se propagent vers le parent. Cela peut entraîner un comportement inattendu dans l'application principale.

Pour empêcher une tâche enfant de s'attacher à sa tâche parente, spécifiez l'option TaskCreationOptions.DenyChildAttach quand vous créez l'objet Task ou Task<TResult> parent. Si une tâche enfant tente de s’attacher à son parent alors que celui-ci spécifie l’option TaskCreationOptions.DenyChildAttach, elle échoue et s’exécute comme si l’option TaskCreationOptions.AttachedToParent n’était pas spécifiée.

Vous pourriez également empêcher une tâche enfant de s'attacher à son parent quand la tâche enfant ne se termine pas en temps voulu. Étant donné qu’une tâche parente ne se termine pas tant que toutes les tâches enfants ne sont pas achevées, une tâche enfant à exécution longue peut entraîner des performances médiocres de la part de l’application globale. Pour obtenir un exemple qui montre comment améliorer les performances de l’application en empêchant une tâche de s’attacher à sa tâche parente, consultez Procédure : empêcher une tâche enfant de s’attacher à son parent.

Voir aussi