Task 和 ThreadPool

点点圈 提交于 2020-02-04 08:18:14

在C#中 TASK 和 ThreadPool 都可以完成多任务并行的工作。但是 TASK实际上是微软定义好的,基于 ThreadPool 的一个类。这里面微软做了很多优化工作。

Task Parallelism (Task Parallel Library) 任务并行库,又被称为TPL,本质上是一个高级版本的.NET线程池。

 

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

     自动调整线程数,提供负载平衡以实现吞吐量最大化,使用资源更少。效率更高

2、对于线程或工作项,可以使用更多的编程控件。
    任务和围绕它们生成的框架提供了一组丰富的 API,这些 API 支持等待、取消、继续、可靠的异常处理、详细状态、自定义计划等功能。

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

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

隐式创建和运行任务

Parallel.Invoke(() => { }, () => AA());

  

显式创建和运行任务

不返回值的任务由 System.Threading.Tasks.Task 类表示。返回值的任务由 System.Threading.Tasks.Task<TResult> 类表示,该类从 Task 继承。任务对象处理基础结构详细信息,并提供可在任务的整个生存期内从调用线程访问的方法和属性。例如,可以随时访问任务的 Status 属性,以确定它是已开始运行、已完成运行、已取消还是引发了异常。状态由 TaskStatus 枚举表示。
在创建任务时,你赋予它一个用户委托,该委托封装该任务将执行的代码。该委托可以表示为命名的委托、匿名方法或 lambda 表达式。lambda 表达式可以包含对命名方法的调用,如下面的示例所示。请注意,该示例包含对 Task.Wait 方法的调用,以确保任务在控制台模式应用程序结束之前完成执行。

        // 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();

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

 // 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();

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

      // Better: Create and start the task in one operation. 
      Task taskA = Task.Factory.StartNew(() => Console.WriteLine("Hello from taskA."));

      // Output a message from the calling thread.
      Console.WriteLine("Hello from thread '{0}'.", 
                        Thread.CurrentThread.Name);

      taskA.Wait();     

  Task 和 Task<TResult> 均公开静态 Factory 属性,该属性返回 TaskFactory 的默认实例,因此你可以调用该方法为 Task.Factory.StartNew()。此外,在以下示例中,由于任务的类型为 System.Threading.Tasks.Task<TResult>,因此每个任务都具有包含计算结果的公共 Task<TResult>.Result 属性。任务以异步方式运行,可以按任意顺序完成。如果在计算完成之前访问 Result 属性,则该属性将阻止调用线程,直到值可用为止。

static void Main(string[] args)
        {

            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;
        }

  

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

            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);     

 

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

            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}, {3}",
                                      data.Name, data.CreationTime, data.ThreadNum, task.Status);
            }  

  

创建任务延续
  使用 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 属性使用。

            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);

  与上面的代码相同的功能

      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);

  

创建分离的子任务
  如果在任务中运行的用户代码创建一个新任务,且未指定 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.");
            //请注意,父任务不会等待分离子任务完成。

  

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

            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.");

  

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

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

主线程依赖于任务计算的最终结果。
你必须处理可能从任务引发的异常。
应用程序可以在所有任务执行完毕之前终止。例如,执行 Main(应用程序入口点)中的所有同步代码后,控制台应用程序将立即终止。

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

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

  

参考文档:

 

https://msdn.microsoft.com/zh-cn/library/dd537609.aspx

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!