多线程——认识和使用Task

转自:http://www.cnblogs.com/yunfeifei/p/4106318.html

对于多线程,我们经常使用的是Thread。在我们了解Task之前,如果我们要使用多核的功能可能就会自己来开线程,然而这种线程模型在.net 4.0之后被一种称为基于“任务的编程模型”所冲击,因为task会比thread具有更小的性能开销,不过大家肯定会有疑惑,任务和线程到底有什么区别呢?

 任务和线程的区别:

1、任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。

2、任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制。

 

 一、认识Task和Task的基本使用

1、认识Task

首先来看一下Task的继承结构。Task标识一个异步操作。

多线程——认识和使用Task_第1张图片

可以看到Task和Thread一样,位于System.Threading命名空间下,这也就是说他们直接有密不可分的联系。下面我们来仔细看一下吧!

 

2、创建Task

创建Task的方法有两种,一种是直接创建——new一个出来,一种是通过工厂创建。下面来看一下这两种创建方法:

        //第一种创建方式,直接实例化
         var task1 = new Task(() =>
         {
            //TODO you code
         });

这是最简单的创建方法,可以看到其构造函数是一个Action,其构造函数有如下几种,比较常用的是前两种。

多线程——认识和使用Task_第2张图片

        //第二种创建方式,工厂创建
         var task2 = Task.Factory.StartNew(() =>
         {
            //TODO you code
         });

这种方式通过静态工厂,创建以个Task并运行。下面我们来建一个控制台项目,演示一下,代码如下:

要添加System.Threading.Tasks命名控件引用。

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

namespace TaskDemo
{
   class Program
   {
      static void Main(string[] args)
      {
         var task1 = new Task(() =>
         {
            Console.WriteLine("Hello,task");
         });
         task1.Start();

         var task2 = Task.Factory.StartNew(() =>
         {
            Console.WriteLine("Hello,task started by task factory");
         });

         Console.Read();
      }
   }
}

 这里我分别用两种方式创建两个task,并让他们运行。可以看到通过构造函数创建的task,必须手动Start,而通过工厂创建的Task直接就启动了。

下面我们来看一下Task的声明周期,编写如下代码:

      var task1 = new Task(() =>
         {
            Console.WriteLine("Begin");
            System.Threading.Thread.Sleep(2000);
            Console.WriteLine("Finish");
         });
         Console.WriteLine("Before start:" + task1.Status);
         task1.Start();
         Console.WriteLine("After start:" + task1.Status);
         task1.Wait();
         Console.WriteLine("After Finish:" + task1.Status);

         Console.Read();

  task1.Status就是输出task的当前状态,其输出结果如下:

多线程——认识和使用Task_第3张图片

可以看到调用Start前的状态是Created,然后等待分配线程去执行,到最后执行完成。

从我们可以得出Task的简略生命周期:

Created:表示默认初始化任务,但是“工厂创建的”实例直接跳过。

WaitingToRun: 这种状态表示等待任务调度器分配线程给任务执行。

RanToCompletion:任务执行完毕。

 

 二、Task的任务控制

  Task最吸引人的地方就是他的任务控制了,你可以很好的控制task的执行顺序,让多个task有序的工作。下面来详细说一下:

1、Task.Wait

在上个例子中,我们已经使用过了,task1.Wait();就是等待任务执行完成,我们可以看到最后task1的状态变为Completed。

 

2、Task.WaitAll

看字面意思就知道,就是等待所有的任务都执行完成,下面我们来写一段代码演示一下:

    static void Main(string[] args)
      {
         var task1 = new Task(() =>
         {
            Console.WriteLine("Task 1 Begin");
            System.Threading.Thread.Sleep(2000);
            Console.WriteLine("Task 1 Finish");
         });
         var task2 = new Task(() =>
         {
            Console.WriteLine("Task 2 Begin");
            System.Threading.Thread.Sleep(3000);
            Console.WriteLine("Task 2 Finish");
         });
         
         task1.Start();
         task2.Start();
         Task.WaitAll(task1, task2);
         Console.WriteLine("All task finished!");

         Console.Read();
      }

其输出结果如下:

多线程——认识和使用Task_第4张图片

可以看到,任务一和任务二都完成以后,才输出All task finished!

 

3、Task.WaitAny

这个用发同Task.WaitAll,就是等待任何一个任务完成就继续向下执行,将上面的代码WaitAll替换为WaitAny,输出结果如下:

多线程——认识和使用Task_第5张图片

 

4、Task.ContinueWith

就是在第一个Task完成后自动启动下一个Task,实现Task的延续,下面我们来看下他的用法,编写如下代码:

static void Main(string[] args)
      {
         var task1 = new Task(() =>
         {
            Console.WriteLine("Task 1 Begin");
            System.Threading.Thread.Sleep(2000);
            Console.WriteLine("Task 1 Finish");
         });
         var task2 = new Task(() =>
         {
            Console.WriteLine("Task 2 Begin");
            System.Threading.Thread.Sleep(3000);
            Console.WriteLine("Task 2 Finish");
         });

         
         task1.Start();
         task2.Start();
         var result = task1.ContinueWith(task =>
         {
            Console.WriteLine("task1 finished!");
            return "This is task result!";
         });

         Console.WriteLine(result.Result.ToString());


         Console.Read();
      }

执行结果如下:

多线程——认识和使用Task_第6张图片

可以看到,task1完成之后,开始执行后面的内容,并且这里我们取得task的返回值。

在每次调用ContinueWith方法时,每次会把上次Task的引用传入进来,以便检测上次Task的状态,比如我们可以使用上次Task的Result属性来获取返回值。我们还可以这么写:

var SendFeedBackTask = Task.Factory.StartNew(() => { Console.WriteLine("Get some Data!"); })
                            .ContinueWith(s => { return true; })
                            .ContinueWith(r => 
                              {
                                 if (r.Result)
                                 {
                                    return "Finished";
                                 }
                                 else
                                 {
                                    return "Error";
                                 }
                              });
         Console.WriteLine(SendFeedBackTask.Result);

首先输出Get some data,然后执行第二个获得返回值true,最后根据判断返回Finished或error。输出结果:

Get some Data!

Finished

其实上面的写法简化一下,可以这样写:

Task.Factory.StartNew(() => {return "One";}).ContinueWith(ss => { Console.WriteLine(ss.Result);});

输出One,这个可以看明白了吧~

 更多ContinueWith用法参见:http://technet.microsoft.com/zh-CN/library/dd321405

 

5、Task的取消

前面说了那么多Task的用法,下面来说下Task的取消,比如我们启动了一个task,出现异常或者用户点击取消等等,我们可以取消这个任务。

如何取消一个Task呢,我们通过cancellation的tokens来取消一个Task。在很多Task的Body里面包含循环,我们可以在轮询的时候判断IsCancellationRequested属性是否为True,如果是True的话就return或者抛出异常,抛出异常后面再说,因为还没有说异常处理的东西。

下面在代码中看下如何实现任务的取消,代码如下:

var tokenSource = new CancellationTokenSource();
         var token = tokenSource.Token;
         var task = Task.Factory.StartNew(() =>
         {
            for (var i = 0; i < 1000; i++)
            {
               System.Threading.Thread.Sleep(1000);
               if (token.IsCancellationRequested)
               {
                  Console.WriteLine("Abort mission success!");
                  return;
               }
            }
         }, token);
         token.Register(() =>
         {
            Console.WriteLine("Canceled");
         });
         Console.WriteLine("Press enter to cancel task...");
         Console.ReadKey();
         tokenSource.Cancel();

         Console.ReadKey();//这句忘了加,程序退出了,看不到“Abort mission success!“这个提示
 

这里开启了一个Task,并给token注册了一个方法,输出一条信息,然后执行ReadKey开始等待用户输入,用户点击回车后,执行tokenSource.Cancel方法,取消任务。其输出结果如下:

多线程——认识和使用Task_第7张图片

 

 三、Task的嵌套

   Task中还可以再嵌套Task,Thread中能不能这样做,我只能说我是没这样写过。Task中的嵌套,我感觉其实也可以分开来写,不过嵌套起来会方便管理一点。Task中的嵌套分为两种,关联嵌套和非关联嵌套,就是说内层的Task和外层的Task是否有联系,下面我们编写代码先来看一下非关联嵌套,及内层Task和外层Task没有任何关系,还是在控制台程序下面,代码如下:

static void Main(string[] args)
      {
         var pTask = Task.Factory.StartNew(() => 
         {
            var cTask = Task.Factory.StartNew(() =>
            {
               System.Threading.Thread.Sleep(2000);
               Console.WriteLine("Childen task finished!");
            });
            Console.WriteLine("Parent task finished!");
         });
         pTask.Wait();
         Console.WriteLine("Flag");
         Console.Read();
      }

 

运行后,输出以下信息:

多线程——认识和使用Task_第8张图片

从图中我们可以看到,外层的pTask运行完后,并不会等待内层的cTask,直接向下走先输出了Flag。这种嵌套有时候相当于我们创建两个Task,但是嵌套在一起的话,在Task比较多时会方便查找和管理,并且还可以在一个Task中途加入多个Task,让进度并行前进。

下面我们来看一下如何创建关联嵌套,就是创建有父子关系的Task,修改上面代码如下:

    static void Main(string[] args)
      {
         var pTask = Task.Factory.StartNew(() => 
         {
            var cTask = Task.Factory.StartNew(() =>
            {
               System.Threading.Thread.Sleep(2000);
               Console.WriteLine("Childen task finished!");
            },TaskCreationOptions.AttachedToParent);
            Console.WriteLine("Parent task finished!");
         });
         pTask.Wait();
         Console.WriteLine("Flag");
         Console.Read();
      }

可以看到,我们在创建cTask时,加入了以参数,TaskCreationOptions.AttachedToParent,这个时候,cTask和pTask就会建立关联,cTask就会成为pTask的一部分,运行代码,看下结果:

多线程——认识和使用Task_第9张图片

可以看到,tTask会等待cTask执行完成。省得我们写Task.WaitAll了,外层的Task会自动等待所有的子Task完成才向下走。

 

下面我们来写一个Task综合使用的例子,来看一下多任务是如何协作的。假设有如下任务,如图:

多线程——认识和使用Task_第10张图片

任务2和任务3要等待任务1完成后,取得任务1的结果,然后开始执行。任务4要等待任务2完成,取得其结果才能执行,最终任务3和任务4都完成了,合并结果,任务完成。图中已经说的很明白了。下面来看一下代码:

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

namespace TaskDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Task.Factory.StartNew(() =>
            {
                var t1 = Task.Factory.StartNew(() => 
                {
                    Console.WriteLine("Task 1 running...");
                    return 1;
                });
                t1.Wait(); //等待任务一完成
                var t3 = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("Task 3 running...");
                    return t1.Result + 3;
                });
                var t4 = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("Task 2 running...");
                    return t1.Result + 2;
                }).ContinueWith(task =>
                {
                    Console.WriteLine("Task 4 running...");
                    return task.Result + 4;
                });
                Task.WaitAll(t3, t4);  //等待任务三和任务四完成
                var result = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("Task Finished! The result is {0}",t3.Result + t4.Result);
                });
            });
            Console.Read();
        }
    }
}

任务2和任务4可以用ContinueWith连接执行,最终运行结果如图:

多线程——认识和使用Task_第11张图片

可以看到所有的任务都执行了,我们也得到了正确的结果11.这下体会到Task的强大了吧~

 

 四、Task的异常处理

   任何应用程序都需要有异常处理机制,谁也不能保证自己写到代码在任何时候都是可以正常运行的,那么在Task中到底该怎么处理异常呢?先来按照平时的写法,加个Try...Catch...试试,看看会出现什么现象:

   static void Main(string[] args)
      {
         try
         {
            var pTask = Task.Factory.StartNew(() =>
            {
               var cTask = Task.Factory.StartNew(() =>
               {
                  System.Threading.Thread.Sleep(2000);
                  throw new Exception("cTask Error!");
                  Console.WriteLine("Childen task finished!");
               });
               throw new Exception("pTask Error!");
               Console.WriteLine("Parent task finished!");
            });

            pTask.Wait();
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }
         Console.WriteLine("Flag");
         Console.Read();
      }

 

大家都看得懂,就不解释了,直接F5运行,结果如图:

多线程——认识和使用Task_第12张图片

唉,不对啊~~怎么显示这异常信息呢?先不说异常信息对不对,反正异常是捕获到了。从这张图中你们还发现了什么吗?

没错,cTask被中断了,这里cTask和pTask并没有建立关联,但是pTask出现异常,其内部的Task也都会中断,不再执行,即使异常是在子Task启动以后发生的。

下面我们继续来说异常吧,来看看正确的异常处理办法,怎么捕获到真正的异常信息,代码如下:

     static void Main(string[] args)
      {
         try
         {
            var pTask = Task.Factory.StartNew(() =>
            {
               var cTask = Task.Factory.StartNew(() =>
               {
                  System.Threading.Thread.Sleep(2000);
                  throw new Exception("cTask Error!");
                  Console.WriteLine("Childen task finished!");
               });
               throw new Exception("pTask Error!");
               Console.WriteLine("Parent task finished!");
            });

            pTask.Wait();
         }
         catch (AggregateException ex)
         {
            foreach (Exception inner in ex.InnerExceptions)
            {
               Console.WriteLine(inner.Message);
            }
         }
         Console.WriteLine("Flag");
         Console.Read();
      }

这里用了AggregateException,就是异常集合,当然开发中不会只有一个线程,肯定会有多个线程,多个线程就可能有多个异常。我们变量异常集合,输出异常信息,如下图:

多线程——认识和使用Task_第13张图片

对了吧,看到正确的异常信息了,但是还是看不到cTask的,因为他被中断了。

当然,除了在task中使用异常,我们还可以通过Task的几个属性来判断Task的状态,如:IsCompleted, IsFaulted, IsCancelled,Exception等等来判断task是否成功的执行了。

你可能感兴趣的:(C#,多线程,Task)