.Net基于任务的异步编程(笔记)

本文是阅读微软官方教程——基于任务的异步编程,做的笔记,以备日后参考。

目录

1.概述

2.隐式创建和运行任务

3.显示创建和运行任务

3.1 使用Task类的构造函数创建任务

3.2 使用Task.Run函数创建并运行任务

3.3 使用TaskFactory.StartNew 创建并运行任务

3.4 使用Result属性返回运行结果

3.5 使用Lambda创建任务

3.6 利用对象参数保存状态量

3.7 利用AsyncState属性访问状态参数

4. 创建任务选项

5. 创建任务延续

5.1 创建单一链接任务

 5.2 多任务继续

6.创建子任务(嵌套任务)

6.1 创建子任务

6.2 创建附加的子任务

6.3 两种任务的对比

6.4 子任务的异常

6.5 任务取消

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

 7. 等待任务

8. 组合任务

Task.WhenAll

Task.WhenAny

Task.Delay

Task(T).FromResult

9. 处理任务中的异常

10. 取消任务

11. TaskFactory类

12. 自定义计划程序

13. 相关数据结构



1.概述

任务并行库 (TPL) 以“任务” 的概念为基础,后者表示异步操作。 在某些方面,任务类似于线程或 ThreadPool 工作项,但是抽象级别更高。 术语“任务并行” 是指一个或多个独立的任务同时运行。 任务提供两个主要好处:

  • 系统资源的使用效率更高,可伸缩性更好。

    在后台,任务排队到已使用算法增强的 ThreadPool,这些算法能够确定线程数并随之调整,提供负载平衡以实现吞吐量最大化。 这会使任务相对轻量,你可以创建很多任务以启用细化并行。

  • 对于线程或工作项,可以使用更多的编程控件。

    任务和围绕它们生成的框架提供了一组丰富的 API,这些 API 支持等待、取消、继续、可靠的异常处理、详细状态、自定义计划等功能。

出于这两个原因,在 .NET Framework 中,TPL 是用于编写多线程、异步和并行代码的首选 API。

2.隐式创建和运行任务

Parallel.Invoke 方法提供了一种简便方式,可同时运行任意数量的任意语句。 只需为每个工作项传入 Action 委托即可。 创建这些委托的最简单方式是使用 lambda 表达式。 lambda 表达式可调用指定的方法,或提供内联代码。 下面的示例演示一个基本的 Invoke 调用,该调用创建并启动同时运行的两个任务。 第一个任务由调用名为 DoSomeWork 的方法的 lambda 表达式表示,第二个任务由调用名为 DoSomeOtherWork 的方法的 lambda 表达式表示。

public static void Invoke (params Action[] actions);

actions是要执行的action数组,可以传递任意多个action。官方例子:

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

    class ParallelInvokeDemo
    {

        // Demonstrated features:
        // 		Parallel.Invoke()
        // Expected results:
        // 		The threads on which each task gets executed may be different.
        //		The thread assignments may be different in different executions.
        //		The tasks may get executed in any order.
        // Documentation:
        //		http://msdn.microsoft.com/library/dd783942(VS.100).aspx
        static void Main()
        {
            try
            {
                Parallel.Invoke(
                    BasicAction,	// Param #0 - static method
                    () =>			// Param #1 - lambda expression
                    {
                        Console.WriteLine("Method=beta, Thread={0}", Thread.CurrentThread.ManagedThreadId);
                    },
                    delegate()		// Param #2 - in-line delegate
                    {
                        Console.WriteLine("Method=gamma, Thread={0}", Thread.CurrentThread.ManagedThreadId);
                    }
                );
            }
            // No exception is expected in this example, but if one is still thrown from a task,
            // it will be wrapped in AggregateException and propagated to the main thread.
            catch (AggregateException e)
            {
                Console.WriteLine("An action has thrown an exception. THIS WAS UNEXPECTED.\n{0}", e.InnerException.ToString());
            }
        }

        static void BasicAction()
        {
            Console.WriteLine("Method=alpha, Thread={0}", Thread.CurrentThread.ManagedThreadId);
        }


    }

Task 在后台创建的 Invoke 实例数不一定与所提供的委托数相等。 TPL 可能会使用各种优化,特别是对于大量的委托。为了更好地控制任务执行或从任务返回值,必须更加显式地使用 Task 对象。

3.显示创建和运行任务

3.1 使用Task类的构造函数创建任务

不返回值的任务由 System.Threading.Tasks.Task 类表示。 返回值的任务由 System.Threading.Tasks.Task<TResult> 类表示,该类从 Task 继承。 任务对象处理基础结构详细信息,并提供可在任务的整个生存期内从调用线程访问的方法和属性。 例如,可以随时访问任务的 Status 属性,以确定它是已开始运行、已完成运行、已取消还是引发了异常。 状态由 TaskStatus 枚举表示。

在创建任务时,你赋予它一个用户委托,该委托封装该任务将执行的代码。 该委托可以表示为命名的委托、匿名方法或 lambda 表达式。 lambda 表达式可以包含对命名方法的调用,如下面的示例所示。 请注意,该示例包含对 Task.Wait 方法的调用,以确保任务在控制台模式应用程序结束之前完成执行。

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

public class Example
{
   public static void Main()
   {
      Thread.CurrentThread.Name = "Main";

      // Create a task and supply a user delegate by using a lambda expression. 
      Task taskA = new Task( () => Console.WriteLine("Hello from taskA."));
      // Start the task.
      taskA.Start();

      // Output a message from the calling thread.
      Console.WriteLine("Hello from thread '{0}'.", 
                        Thread.CurrentThread.Name);
      taskA.Wait();
   }
}
// The example displays output like the following:
//       Hello from thread 'Main'.
//       Hello from taskA.

3.2 使用Task.Run函数创建并运行任务

你还可以使用 Task.Run 方法通过一个操作创建并启动任务。 无论是哪个任务计划程序与当前线程关联,Run 方法都将使用默认的任务计划程序来管理任务。 不需要对任务的创建和计划进行更多控制时,首选 Run 方法创建并启动任务。

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

public class Example
{
   public static void Main()
   {
      Thread.CurrentThread.Name = "Main";

      // Define and run the task.
      Task taskA = Task.Run( () => Console.WriteLine("Hello from taskA."));

      // Output a message from the calling thread.
      Console.WriteLine("Hello from thread '{0}'.", 
                          Thread.CurrentThread.Name);
      taskA.Wait();
   }
}
// The example displays output like the following:
//       Hello from thread 'Main'.
//       Hello from taskA.

3.3 使用TaskFactory.StartNew 创建并运行任务

还可以使用 TaskFactory.StartNew 方法在一个操作中创建并启动任务。 不必将创建和计划分开并且需要其他任务创建选项或使用特定计划程序时,或者需要将其他状态传递到可以通过 Task.AsyncState 属性检索到的任务时,请使用此方法,如下例所示。

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

class CustomData
{
   public long CreationTime;
   public int Name; 
   public int ThreadNum;
}

public class Example
{
   public static void Main()
   {
      Task[] taskArray = new Task[10];
      for (int i = 0; i < taskArray.Length; i++) {
         taskArray[i] = Task.Factory.StartNew( (Object obj ) => {
                                                  CustomData data = obj as CustomData;
                                                  if (data == null) 
                                                     return;
                                     
                                                  data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                               },
                                               new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks} );
      }
      Task.WaitAll(taskArray);     
      foreach (var task in taskArray) {
         var data = task.AsyncState as CustomData;
         if (data != null)
            Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                              data.Name, data.CreationTime, data.ThreadNum);
      }                     
   }
}
// The example displays output like the following:
//       Task #0 created at 635116412924597583 on thread #3.
//       Task #1 created at 635116412924607584 on thread #4.
//       Task #3 created at 635116412924607584 on thread #4.
//       Task #4 created at 635116412924607584 on thread #4.
//       Task #2 created at 635116412924607584 on thread #3.
//       Task #6 created at 635116412924607584 on thread #3.
//       Task #5 created at 635116412924607584 on thread #4.
//       Task #8 created at 635116412924607584 on thread #4.
//       Task #7 created at 635116412924607584 on thread #3.
//       Task #9 created at 635116412924607584 on thread #4.

3.4 使用Result属性返回运行结果

由于任务的类型为 System.Threading.Tasks.Task<TResult>,因此每个任务都具有包含计算结果的公共 Task<TResult>.Result 属性。 任务以异步方式运行,可以按任意顺序完成。 如果在计算完成之前访问 Result 属性,则该属性将阻止调用线程,直到值可用为止。

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
        Task<Double>[] taskArray = { Task<Double>.Factory.StartNew(() => DoComputation(1.0)),
                                     Task<Double>.Factory.StartNew(() => DoComputation(100.0)), 
                                     Task<Double>.Factory.StartNew(() => DoComputation(1000.0)) };

        var results = new Double[taskArray.Length];
        Double sum = 0;
        
        for (int i = 0; i < taskArray.Length; i++) {
            results[i] = taskArray[i].Result;
            Console.Write("{0:N1} {1}", results[i], 
                              i == taskArray.Length - 1 ? "= " : "+ ");
            sum += results[i];
        }
        Console.WriteLine("{0:N1}", sum);
   }

   private static Double DoComputation(Double start)
   {
      Double sum = 0;
      for (var value = start; value <= start + 10; value += .1)
         sum += value;

      return sum; 
   }
}
// The example displays the following output:
//        606.0 + 10,605.0 + 100,495.0 = 111,706.0

3.5 使用Lambda创建任务

使用 lambda 表达式创建委托时,你有权访问源代码中当时可见的所有变量。 然而,在某些情况下,特别是在循环中,lambda 不按照预期的方式捕获变量。 它仅捕获最终值,而不是它每次迭代后更改的值。 以下示例演示了该问题。 它将循环计数器传递给实例化 CustomData 对象并使用循环计数器作为对象标识符的 lambda 表达式。 如示例输出所示,每个 CustomData 对象都具有相同的标识符。

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

class CustomData
{
   public long CreationTime;
   public int Name; 
   public int ThreadNum;
}

public class Example
{
   public static void Main()
   {
      // Create the task object by using an Action(Of Object) to pass in the loop
      // counter. This produces an unexpected result.
      Task[] taskArray = new Task[10];
      for (int i = 0; i < taskArray.Length; i++) {
         taskArray[i] = Task.Factory.StartNew( (Object obj) => {
                                                 var data = new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks}; 
                                                 data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                                 Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                   data.Name, data.CreationTime, data.ThreadNum);
                                               },
                                              i );
      }
      Task.WaitAll(taskArray);     
   }
}
// The example displays output like the following:
//       Task #10 created at 635116418427727841 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427727841 on thread #3.
//       Task #10 created at 635116418427747843 on thread #3.
//       Task #10 created at 635116418427747843 on thread #3.
//       Task #10 created at 635116418427737842 on thread #4.

如果我们就要访问每次迭代的值呢?

3.6 利用对象参数保存状态量

通过使用构造函数向任务提供状态对象,可以在每次迭代时访问该值。 以下示例在上一示例的基础上做了修改,在创建 CustomData 对象时使用循环计数器,该对象继而传递给 lambda 表达式。 如示例输出所示,每个 CustomData 对象现在都具有唯一的一个标识符,该标识符基于该对象实例化时循环计数器的值。

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

class CustomData
{
   public long CreationTime;
   public int Name; 
   public int ThreadNum;
}

public class Example
{
   public static void Main()
   {
      // Create the task object by using an Action(Of Object) to pass in custom data
      // to the Task constructor. This is useful when you need to capture outer variables
      // from within a loop. 
      Task[] taskArray = new Task[10];
      for (int i = 0; i < taskArray.Length; i++) {
         taskArray[i] = Task.Factory.StartNew( (Object obj ) => {
                                                  CustomData data = obj as CustomData;
                                                  if (data == null) 
                                                     return;
                                     
                                                  data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                                  Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                   data.Name, data.CreationTime, data.ThreadNum);
                                               },
                                               new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks} );
      }
      Task.WaitAll(taskArray);     
   }
}
// The example displays output like the following:
//       Task #0 created at 635116412924597583 on thread #3.
//       Task #1 created at 635116412924607584 on thread #4.
//       Task #3 created at 635116412924607584 on thread #4.
//       Task #4 created at 635116412924607584 on thread #4.
//       Task #2 created at 635116412924607584 on thread #3.
//       Task #6 created at 635116412924607584 on thread #3.
//       Task #5 created at 635116412924607584 on thread #4.
//       Task #8 created at 635116412924607584 on thread #4.
//       Task #7 created at 635116412924607584 on thread #3.
//       Task #9 created at 635116412924607584 on thread #4.

3.7 利用AsyncState属性访问状态参数

将变量i作为构造函数的参数传递进去,就可以保存其状态量。此状态作为参数传递给任务委托,并且可通过使用 Task.AsyncState 属性从任务对象访问。 以下示例在上一示例的基础上演变而来。 它使用 AsyncState 属性显示关于传递到 lambda 表达式的 CustomData 对象的信息。

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

class CustomData
{
   public long CreationTime;
   public int Name; 
   public int ThreadNum;
}

public class Example
{
   public static void Main()
   {
      Task[] taskArray = new Task[10];
      for (int i = 0; i < taskArray.Length; i++) {
         taskArray[i] = Task.Factory.StartNew( (Object obj ) => {
                                                  CustomData data = obj as CustomData;
                                                  if (data == null) 
                                                     return;
                                     
                                                  data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                               },
                                               new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks} );
      }
      Task.WaitAll(taskArray);     
      foreach (var task in taskArray) {
         var data = task.AsyncState as CustomData;
         if (data != null)
            Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                              data.Name, data.CreationTime, data.ThreadNum);
      }                     
   }
}
// The example displays output like the following:
//       Task #0 created at 635116412924597583 on thread #3.
//       Task #1 created at 635116412924607584 on thread #4.
//       Task #3 created at 635116412924607584 on thread #4.
//       Task #4 created at 635116412924607584 on thread #4.
//       Task #2 created at 635116412924607584 on thread #3.
//       Task #6 created at 635116412924607584 on thread #3.
//       Task #5 created at 635116412924607584 on thread #4.
//       Task #8 created at 635116412924607584 on thread #4.
//       Task #7 created at 635116412924607584 on thread #3.
//       Task #9 created at 635116412924607584 on thread #4.

每个任务都获得一个在应用程序域中唯一标识自己的整数 ID,可以使用 Task.Id 属性访问该 ID。 该 ID 可有效用于在 Visual Studio 调试器的“并行堆栈” 和“任务” 窗口中查看任务信息。 该 ID 是惰式创建的,这意味着它不会在被请求之前创建;因此每次运行该程序时,任务可能具有不同的 ID。


4. 创建任务选项

Task的一个构造函数为:

public Task (Action action, System.Threading.Tasks.TaskCreationOptions creationOptions);

其中creationOption用于自定义任务行为。它有以下选项:

TaskCreationOptions 参数值说明
None未指定任何选项时的默认值。 计划程序将使用其默认试探法来计划任务。
PreferFairness指定应当计划任务,以使越早创建的任务将更可能越早执行,而越晚创建的任务将更可能越晚执行。
LongRunning指定该任务表示长时间运行的运算。
AttachedToParent指定应将任务创建为当前任务(如果存在)的附加子级。 有关详细信息,请参阅附加和分离的子任务
DenyChildAttach指定如果内部任务指定 AttachedToParent 选项,则该任务不会成为附加的子任务。
HideScheduler指定通过调用特定任务内部的 TaskFactory.StartNew 或 Task<TResult>.ContinueWith 等方法创建的任务的任务计划程序是默认计划程序,而不是正在运行此任务的计划程序。

可以通过使用位 OR 运算组合选项。 下面的示例演示一个具有 LongRunning 和 PreferFairness 选项的任务。

var task3 = new Task(() => MyLongRunningMethod(),
                    TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task3.Start();

5. 创建任务延续

5.1 创建单一链接任务

使用 Task.ContinueWith 和 Task<TResult>.ContinueWith 方法,可以指定要在先行任务 完成时启动的任务。 延续任务的委托已传递了对先行任务的引用,因此它可以检查先行任务的状态,并通过检索 Task<TResult>.Result 属性的值将先行任务的输出用作延续任务的输入。

在下面的示例中,getData 任务通过调用 TaskFactory.StartNew<TResult>(Func<TResult>) 方法来启动。 当 processData完成时,getData 任务自动启动,当 displayData 完成时,processData 启动。 getData 产生一个整数数组,通过 processData 任务的 getData 属性,Task<TResult>.Result 任务可访问该数组。 processData 任务处理该数组并返回结果,结果的类型从传递到 Task<TResult>.ContinueWith<TNewResult>(Func<Task<TResult>,TNewResult>) 方法的 Lambda 表达式的返回类型推断而来。 displayData 完成时,processData 任务自动执行,而 Tuple<T1,T2,T3> 任务可通过 processData 任务的 displayData 属性访问由 processData lambda 表达式返回的 Task<TResult>.Result 对象。displayData 任务采用 processData 任务的结果,继而得出自己的结果,其类型以相似方式推断而来,且可由程序中的 Result 属性使用。

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {                         
      var getData = Task.Factory.StartNew(() => { 
                                             Random rnd = new Random(); 
                                             int[] values = new int[100];
                                             for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
                                                values[ctr] = rnd.Next();

                                             return values;
                                          } );  
      var processData = getData.ContinueWith((x) => {
                                                int n = x.Result.Length;
                                                long sum = 0;
                                                double mean;
                                  
                                                for (int ctr = 0; ctr <= x.Result.GetUpperBound(0); ctr++)
                                                   sum += x.Result[ctr];

                                                mean = sum / (double) n;
                                                return Tuple.Create(n, sum, mean);
                                             } ); 
      var displayData = processData.ContinueWith((x) => {
                                                    return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                                         x.Result.Item1, x.Result.Item2, 
                                                                         x.Result.Item3);
                                                 } );                         
      Console.WriteLine(displayData.Result);
   }
}
// The example displays output similar to the following:
//    N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82

因为 Task.ContinueWith 是实例方法,所以你可以将方法调用链接在一起,而不是为每个先行任务去实例化 Task<TResult>对象。 以下示例与上一示例在功能上等同,唯一的不同在于它将对 Task.ContinueWith 方法的调用链接在一起。 请注意,通过方法调用链返回的 Task<TResult> 对象是最终延续任务。 

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {                         
      var displayData = Task.Factory.StartNew(() => { 
                                                 Random rnd = new Random(); 
                                                 int[] values = new int[100];
                                                 for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
                                                    values[ctr] = rnd.Next();

                                                 return values;
                                              } ).  
                        ContinueWith((x) => {
                                        int n = x.Result.Length;
                                        long sum = 0;
                                        double mean;
                                  
                                        for (int ctr = 0; ctr <= x.Result.GetUpperBound(0); ctr++)
                                           sum += x.Result[ctr];

                                        mean = sum / (double) n;
                                        return Tuple.Create(n, sum, mean);
                                     } ). 
                        ContinueWith((x) => {
                                        return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                             x.Result.Item1, x.Result.Item2, 
                                                             x.Result.Item3);
                                     } );                         
      Console.WriteLine(displayData.Result);
   }
}
// The example displays output similar to the following:
//    N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82

 完全通过链式实现。

 5.2 多任务继续

使用 ContinueWhenAll 和 ContinueWhenAny 方法,可以从多个任务继续。这个话题可以单独展开为一个专题。


6.创建子任务(嵌套任务)

6.1 创建子任务

如果在任务中运行的用户代码创建一个新任务,且未指定 AttachedToParent 选项,则该新任务不采用任何特殊方式与父任务同步。 这种不同步的任务类型称为“分离的嵌套任务” 或“分离的子任务” 。 以下示例展示了创建一个分离子任务的任务。

 

var outer = Task.Factory.StartNew(() =>
{
    Console.WriteLine("Outer task beginning.");

    var child = Task.Factory.StartNew(() =>
    {
        Thread.SpinWait(5000000);
        Console.WriteLine("Detached task completed.");
    });

});

outer.Wait();
Console.WriteLine("Outer task completed.");
// The example displays the following output:
//    Outer task beginning.
//    Outer task completed.
//    Detached task completed.

请注意,父任务不会等待分离子任务完成

6.2 创建附加的子任务

如果任务中运行的用户代码在创建任务时指定了 AttachedToParent 选项,新任务就称为父任务的附加子任务 。 因为父任务隐式地等待所有附加子任务完成,所以你可以使用 AttachedToParent 选项表示结构化的任务并行。 以下示例展示了创建十个附加子任务的父任务。 请注意,虽然此示例调用 Task.Wait 方法等待父任务完成,但不必显式等待附加子任务完成。

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 beginning.");
                      for (int ctr = 0; ctr < 10; ctr++) {
                         int taskNo = ctr;
                         Task.Factory.StartNew((x) => {
                                                  Thread.SpinWait(5000000);
                                                  Console.WriteLine("Attached child #{0} completed.", 
                                                                    x);
                                               },
                                               taskNo, TaskCreationOptions.AttachedToParent);
                      }
                   });

      parent.Wait();
      Console.WriteLine("Parent task completed.");
   }
}
// The example displays output like the following:
//       Parent task beginning.
//       Attached child #9 completed.
//       Attached child #0 completed.
//       Attached child #8 completed.
//       Attached child #1 completed.
//       Attached child #7 completed.
//       Attached child #2 completed.
//       Attached child #6 completed.
//       Attached child #3 completed.
//       Attached child #5 completed.
//       Attached child #4 completed.
//       Parent task completed.

父任务可使用 TaskCreationOptions.DenyChildAttach 选项阻止其他任务附加到父任务。

6.3 两种任务的对比

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

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

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

在大多数情况下,我们建议你使用分离子任务,因为它们与其他任务之间的关系不太复杂。 这就是父任务内创建的任务会默认分离的原因,并且必须显式指定 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("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.

如果该子任务由 Task<TResult> 对象,而不是 Task 对象表示,则你可以通过访问子任务的 Task<TResult>.Result 属性,确保父任务将等待子任务完成,即使该子任务是一个分离子任务。 Result 属性在其任务完成前会进行阻止,即——保证子任务先完成。如以下示例所示。

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.

不同于分离子任务,附加子任务与父任务紧密同步。 可以通过使用任务创建语句中的 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.

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

但是,子任务仅在其父任务不会阻止附加子任务时,才可以附加到其父任务。 通过在父任务类构造函数中指定 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 Example
{
   public static void Main()
   {
      var parent = Task.Run(() => {
            Console.WriteLine("Parent task executing.");
            var child = Task.Factory.StartNew(() => {
                  Console.WriteLine("Child starting.");
                  Thread.SpinWait(5000000);
                  Console.WriteLine("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.

6.4 子任务的异常

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

6.5 任务取消

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

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

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

若要防止子任务附加到其父任务,请在创建父任务 Task 或 Task<TResult> 对象时,指定 TaskCreationOptions.DenyChildAttach 选项。 当某项任务尝试附加到其父任务,且其父任务指定了 TaskCreationOptions.DenyChildAttach 选项时,则子任务将不能附加到父任务,并且将像未指定 TaskCreationOptions.AttachedToParent 选项一样进行执行。

可能还想要防止子任务在没有及时完成时附加到其父任务。 因为父任务只有在所有子任务完成后才会完成,所以长时间运行的子任务会使整个应用执行得非常缓慢。 有关演示如何通过防止子任务附加到父任务来提升应用性能的示例,请参阅如何:防止子任务附加到父任务。

 7. 等待任务

System.Threading.Tasks.Task 和 System.Threading.Tasks.Task<TResult> 类型提供了 Task.Wait 方法的若干重载,以便能够等待任务完成。 此外,使用静态 Task.WaitAll 和 Task.WaitAny 方法的重载可以等待一批任务中的任一任务或所有任务完成。

通常,会出于以下某个原因等待任务:

  • 主线程依赖于任务计算的最终结果。

  • 你必须处理可能从任务引发的异常。

  • 应用程序可以在所有任务执行完毕之前终止。 例如,执行 Main(应用程序入口点)中的所有同步代码后,控制台应用程序将立即终止。

下面的示例演示不包含异常处理的基本模式。

Task[] tasks = new Task[3]
{
    Task.Factory.StartNew(() => MethodA()),
    Task.Factory.StartNew(() => MethodB()),
    Task.Factory.StartNew(() => MethodC())
};

//Block until all tasks complete.
Task.WaitAll(tasks);

// Continue on this thread...

某些重载允许你指定超时,而其他重载采用额外的 CancellationToken 作为输入参数,以便可以通过编程方式或根据用户输入来取消等待。

等待任务时,其实是在隐式等待使用 TaskCreationOptions.AttachedToParent 选项创建的该任务的所有子级。 Task.Wait 在该任务已完成时立即返回。 Task.Wait 方法将抛出由某任务引发的任何异常,即使 Task.Wait 方法是在该任务完成之后调用的。

8. 组合任务

Task 类和 Task<TResult> 类提供多种方法,这些方法能够帮助你组合多个任务以实现常见模式,并更好地使用由 C#、Visual Basic 和 F# 提供的异步语言功能。 本节介绍了 WhenAllWhenAnyDelay 和 FromResult 方法。

Task.WhenAll

Task.WhenAll 方法异步等待多个 Task 或 Task<TResult> 对象完成。 通过它提供的重载版本可以等待非均匀任务组。 例如,你可以等待多个 Task 和 Task<TResult> 对象在一个方法调用中完成。

Task.WhenAny

Task.WhenAny 方法异步等待多个 Task 或 Task<TResult> 对象中的一个完成。 与在 Task.WhenAll 方法中一样,该方法提供重载版本,让你能等待非均匀任务组。 WhenAny 方法在下列情境中尤其有用。

  • 冗余运算。 请考虑可以用多种方式执行的算法或运算。 你可使用 WhenAny 方法来选择先完成的运算,然后取消剩余的运算。

  • 交叉运算。 你可启动必须全部完成的多项运算,并使用 WhenAny 方法在每项运算完成时处理结果。 在一项运算完成后,可以启动一个或多个其他任务。

  • 受限制的运算。 你可使用 WhenAny 方法通过限制并发运算的数量来扩展前面的情境。

  • 过期的运算。 你可使用 WhenAny 方法在一个或多个任务与特定时间后完成的任务(例如 Delay 方法返回的任务)间进行选择。 下节描述了 Delay 方法。

Task.Delay

Task.Delay 方法将生成在指定时间后完成的 Task 对象。 你可使用此方法来生成偶尔轮询数据的循环,引入超时,将对用户输入的处理延迟预定的一段时间等。

Task(T).FromResult

通过使用 Task.FromResult 方法,你可以创建包含预计算结果的 Task<TResult> 对象。 执行返回 Task<TResult> 对象的异步运算,且已计算该 Task<TResult> 对象的结果时,此方法将十分有用。 有关使用 FromResult 检索缓存中包含的异步下载运算结果的示例,请参阅如何:创建预先计算的任务

9. 处理任务中的异常

当某个任务抛出一个或多个异常时,异常包装在 AggregateException 异常中。 该异常传播回与该任务联接的线程,通常该线程正在等待该任务完成或该线程访问 Result 属性。 此行为用于强制实施 .NET Framework 策略 - 默认所有未处理的异常应终止进程。 调用代码可以通过使用 try/catch 块中的以下任意方法来处理异常:

联接线程也可以通过在对任务进行垃圾回收之前访问 Exception 属性来处理异常。 通过访问此属性,可防止未处理的异常在对象完成时触发终止进程的异常传播行为。 

有关异常和任务的的详细信息,请参阅异常处理

10. 取消任务

Task 类支持协作取消,并与 .NET Framework 4 中新增的 System.Threading.CancellationTokenSource 类和 System.Threading.CancellationToken 类完全集成。 System.Threading.Tasks.Task 类中的大多数构造函数采用 CancellationToken 对象作为输入参数。 许多 StartNew 和 Run 重载还包括 CancellationToken 参数。

你可以创建标记,并使用 CancellationTokenSource 类在以后某一时间发出取消请求。 可以将该标记作为参数传递给 Task,还可以在执行响应取消请求的工作的用户委托中引用同一标记。

有关详细信息,请参阅任务取消如何:取消任务及其子级

11. TaskFactory类

TaskFactory 类提供静态方法,这些方法封装了用于创建和启动任务和延续任务的一些常用模式。

默认的 TaskFactory 可作为 Task 类或 Task<TResult> 类上的静态属性访问。 你还可以直接实例化 TaskFactory 并指定各种选项,包括 CancellationTokenTaskCreationOptions 选项、TaskContinuationOptions 选项或 TaskScheduler。 创建任务工厂时所指定的任何选项将应用于它创建的所有任务,除非 Task 是通过使用 TaskCreationOptions 枚举创建的(在这种情况下,任务的选项重写任务工厂的选项)。

12. 自定义计划程序

大多数应用程序或库开发人员并不关心任务在哪个处理器上运行、任务如何将其工作与其他任务同步以及如何在 System.Threading.ThreadPool 中计划任务。 他们只需要它在主机上尽可能高效地执行。 如果需要对计划细节进行更细化的控制,可以使用任务并行库在默认任务计划程序上配置一些设置,甚至是提供自定义计划程序。 有关更多信息,请参见TaskScheduler

13. 相关数据结构

TPL 有几种在并行和顺序方案中都有用的新公共类型。 它们包括 System.Collections.Concurrent 命名空间中的一些线程安全的、快速且可缩放的集合类,还包括一些新的同步类型(例如 System.Threading.Semaphore 和 System.Threading.ManualResetEventSlim),对特定类型的工作负荷而言,这些新同步类型比旧的同步类型效率更高。.NET Framework 4 中的其他新类型(例如 System.Threading.Barrier 和 System.Threading.SpinLock)提供了早期版本中未提供的功能。 有关详细信息,请参阅用于并行编程的数据结构。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值