已附加和已分离的子任务

子任务(或嵌套任务)是在另一System.Threading.Tasks.Task个任务的用户委托(称为父任务)中创建的实例。 可以分离或附加子任务。 分离的子任务是独立于父级而执行的任务。 附加的子任务是使用 TaskCreationOptions.AttachedToParent 选项创建的嵌套任务,父级不显式或默认禁止附加任务。 任务可以创建任意数量的附加和分离的子任务,仅受系统资源的限制。

下表列出了两种子任务之间的基本差异。

类别 分离子任务 附加子任务
父级将等待子任务完成。 是的
父级将传播由子任务引发的异常。 是的
父级的状态取决于子级的状态。 是的

在大多数情况下,我们建议你使用分离子任务,因为它们与其他任务之间的关系不太复杂。 这就是默认情况下在父任务中创建的任务被分离的原因,必须显式指定 TaskCreationOptions.AttachedToParent 用于创建附加子任务的选项。

分离子任务

虽然子任务是由父任务创建的,但默认情况下它独立于父任务。 在以下示例中,父任务创建了一个简单的子任务。 如果多次运行示例代码,你可能会注意到该示例的输出与所示不同,并且每次运行代码时输出都可能会更改。 这是因为父任务和子任务彼此独立执行,子任务是一个分离的任务。 该示例仅等待父任务完成,子任务可能不会在控制台应用终止之前执行或完成。

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

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

            Task 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.

如果子任务由 Task<TResult> 对象而不是 Task 对象表示,则可以确保父任务将等待子任务通过访问 Task<TResult>.Result 子任务的属性来完成,即使它是分离的子任务也是如此。 属性 Result 会阻塞直到其任务完成,如以下示例所示。

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

class Example3
{
    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 {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

附加子任务

与分离的子任务不同,附加的子任务与父任务紧密同步。 可以使用任务创建语句中的选项将上一示例中分离的子任务更改为附加子任务 TaskCreationOptions.AttachedToParent ,如以下示例所示。 在此代码中,附加子任务会在父任务之前完成。 因此,每次运行代码时,示例的输出都是相同的。

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.

可以使用附加子任务,创建异步操作的紧密同步关系图。

但是,子任务仅在其父任务不会阻止附加子任务时,才可以附加到其父任务。 父任务可以通过在父任务的类构造函数或TaskCreationOptions.DenyChildAttach方法中指定TaskFactory.StartNew选项来显式阻止子任务附加到它们。 父任务如果通过调用 Task.Run 方法创建,会隐式阻止子任务附加到它们。 以下示例对此进行了说明。 它与前面的示例相同,只不过父任务是通过调用 Task.Run(Action) 方法而不是 TaskFactory.StartNew(Action) 方法创建的。 由于子任务无法附加到其父任务,因此示例的输出不可预知。 因为 Task.Run 重载的默认任务创建选项包括 TaskCreationOptions.DenyChildAttach,所以本示例在功能上等效于“分离子任务”部分中的第一个示例。

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

public class Example2
{
   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.

子任务中的异常

如果分离的子任务引发异常,则必须在父任务中直接观察或处理该异常,就像任何非嵌套任务一样。 如果附加的子任务引发异常,该异常会自动传播到父任务,并传递回等待或尝试访问该任务 Task<TResult>.Result 属性的线程。 因此,通过使用附加子任务,可以一次性处理调用线程上对 Task.Wait 的调用中的所有异常。 有关详细信息,请参阅 异常处理

取消和子任务

任务取消需要彼此协作。 也就是说,若要取消任务,则每个附加或分离的子任务必须监视取消标记的状态。 如果想要通过使用一个取消请求来取消父任务及其所有子任务,则需要将作为参数的相同令牌传递到所有的任务,并在每个任务中提供逻辑,以对每个任务中的请求作出响应。 有关详细信息,请参阅 任务取消如何:取消任务及其子任务

当父任务取消时

如果父任务在其子任务开始前取消了自身,则子任务将永远不会开始。 如果父任务在其子任务已开始后取消了自身,则子任务将完成运行,除非它自己具有取消逻辑。 有关详细信息,请参阅 任务取消

当分离子任务取消时

如果分离子任务使用传递到父任务的相同标记取消自身,且父任务不会等待子任务,则不会传播异常,因为该异常将被视为良性协作取消。 此行为与任何顶级任务的行为相同。

当附加子任务取消时

当附加子任务使用传递到其父任务的相同标记取消自身时,TaskCanceledException 将传播到 AggregateException 中的联接线程。 必须等待父任务,以便你除了所有通过附加子任务的图形传播的错误异常之外,还可以处理所有良性异常。

有关详细信息,请参阅 异常处理

阻止子任务附加到其父任务

由子任务引发的未经处理的异常将传播到父任务中。 可以使用此行为,从一个根任务而无需遍历任务树来观察所有子任务异常。 但是,当父任务不需要来自其他代码的附件时,异常传播可能会有问题。 例如,考虑一个应用程序从 Task 对象调用第三方库组件。 如果第三方库组件还创建一个 Task 对象并指定 TaskCreationOptions.AttachedToParent 将其附加到父任务,则子任务中发生的任何未经处理的异常将传播到父任务。 这可能会导致主应用中出现意外行为。

若要防止子任务附加到其父任务,请在创建父TaskCreationOptions.DenyChildAttach任务或Task对象时指定Task<TResult>该选项。 当任务尝试附加到其父级,而父级指定了TaskCreationOptions.DenyChildAttach选项时,子任务将无法附加到父级,就会像未指定TaskCreationOptions.AttachedToParent选项时那样执行。

可能还想要防止子任务在没有及时完成时附加到其父任务。 由于父任务在完成所有子任务之前不会完成,因此长时间运行的子任务可能会导致整个应用执行不佳。 有关演示如何通过阻止任务附加到其父任务来提高应用性能的示例,请参阅 “如何:防止子任务附加到其父任务”。

另请参阅