任务Task系列之Task初认识
说起多线程,想到更多的是线程Thread,那么我们今天说的任务Task和线程有什么关系呢?
- 任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行;
- 线程和任务并不是一对一的关系,比如开10个任务并不是说就会开10个线程,这一点任务和线程池有点类似,但是任务相比线程池开销更小控制也更为精确;
关于Task
Task标识一个异步操作,创建Task有两种方法,一种类似于创建对象直接new出来,一种是通过工厂创建。
public void CreateTask() { //直接实例化 var task1 = new Task(() => { Console.WriteLine("任务1创建成功!"); }); task1.Start(); //通过静态工厂创建 var task2 = Task.Factory.StartNew(() => { Console.WriteLine("任务2创建成功!"); }); }
下面来看一下一个Task的生命周期,编写测试代码:
public void GetTaskStatus() { //直接实例化 var task1 = new Task(() => { Console.WriteLine("开始"); Thread.Sleep(10000); Console.WriteLine("结束"); }); Console.WriteLine(task1.Status); task1.Start(); Console.WriteLine(task1.Status); task1.Wait(); Console.WriteLine(task1.Status); }
运行结果:
由此可见 一个Task的生命周期中至少包含以下三个阶段:
- Created: Task被创建
- WaitingToRun :等待执行
- RanToCompletion: 任务执行完成
其实至少应该还包含一个阶段:Running表示运行中。
下面谈谈Task的任务控制,这也是Task的优势所在,可以使以前无法控制的并行过程按照自己的需求变得有序可控起来。
Task.Wait
上面的示例中已经用过了,就是等待任务执行完成,阻塞主线程,多用于线程(任务)与主线程同步。
Task.WaitAll
顾名思义,多个任务并行执行时,等待所有任务执行完成,实例代码:
public void WaitAllTask() { var task1 = new Task(() => { Console.WriteLine("任务一开始!"); Thread.Sleep(2000); Console.WriteLine("任务一结束!"); }); var task2 = new Task(() => { Console.WriteLine("任务二开始!"); Thread.Sleep(2000); Console.WriteLine("任务二结束!"); }); task1.Start(); task2.Start(); Task.WaitAll(task1,task2); Console.WriteLine("所有任务执行完成!"); }
执行结果:
可见等到两个任务全部结束后,主线程才提示任务结束。
Task.WaitAny
等待任意一个任务执行结束,上面的示例代码修改下:
task1.Start(); task2.Start(); Task.WaitAny(task1, task2); Console.WriteLine("有任务执行完成!");
执行结果:
可见任务一结束后主线程就提示有任务完成了。
Task.ContinueWith
一个Task任务后自动开启另一个Task,实现Task的延续。实例代码如下:
public void TaskContinue() { var task1 = new Task(() => { Console.WriteLine("任务一开始!"); Thread.Sleep(2000); Console.WriteLine("任务一结束!"); }); var task2 = new Task(() => { Console.WriteLine("任务二开始!"); Thread.Sleep(20000); Console.WriteLine("任务二结束!"); }); task1.Start(); task2.Start(); var result = task1.ContinueWith(task => { Console.WriteLine("任务一完成"); return String.Format("任务一结束后状态{0}",task.Status); }); Console.WriteLine(result.Result.ToString()); }
运行结果:
Task的取消
前面说了那么多Task的用法,下面我们来看Task的取消,比如我们启动了一个Task,出现了遗产或者用户主动选择取消,我们是可以取消这个任务的。在任务Task中,我们通过calcellation的tokens来取消一个Task,有些时候在Task的Body里面会包含循环,我们可以在轮询的时候判断IscancellationRequested的属性是否为True,如果为True的时候就return或者抛出异常。下面在代码中我们看下如何实现任务的取消:
public void TaskCancel() { var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var task = Task.Factory.StartNew(() => { Int32 sec = 0; while (true) { if (token.IsCancellationRequested) { Console.WriteLine("任务被终止,即将退出"); break; } else { Console.WriteLine("计时{0}s", sec); } Thread.Sleep(1000); sec += 1; } },token); token.Register(() => { Console.WriteLine("任务取消"); }); Console.WriteLine("按任意键退出计时"); Console.ReadKey(); tokenSource.Cancel(); }
这里开启了一个Task,并给token注册了一个方法,输出信息,然后等待用户输入,用户输入任意信息后,执行tokenSource.Cancel方法,任务随即被取消,执行结果如下:
Task的嵌套
Task中还可以再嵌套Task,Task中的嵌套可以分为两种。关联嵌套和非关联嵌套,也就是说内层的Task和外层的Task是否有关联,下面我们代码示例来说明:
public void TaskWithTask() { var pTask = Task.Factory.StartNew(() => { var cTask = Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("子任务完成"); }); Console.WriteLine("父任务完成"); }); pTask.Wait(); Console.WriteLine("非关联嵌套"); }
运行结果:
由执行结果可知,外层的pTask运行完成后,并不会等待内层的cTask,直接向下走了,其实这种嵌套相当于我们创建了两个Task,只是写到一起,代码可读性更强,并且可以在一个Task中加入多个Task,让进度并行前进。
再看一个实例:
public void TaskWithTask() { var pTask = Task.Factory.StartNew(() => { var cTask = Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("子任务完成"); },TaskCreationOptions.AttachedToParent); Console.WriteLine("父任务完成"); }); pTask.Wait(); Console.WriteLine("非关联嵌套"); }
我们在创建cTask的时候,加入了参数TaskCreationOptions.AttachedToParent,这个时候pTask和cTask就产生了关联,cTask就会成为pTask的一部分,执行结果:
可见tTask会等待cTask执行完成。
下面我们来看一个task综合使用的例子,看一些多任务是如何协作的,假设这样一个场景:任务2和任务3要等待任务1完成后,取得任务1的结果,然后开始执行。任务4要等待任务2完成,取得其结果才能执行,最终任务3和任务4都完成了,合并结果,任务完成。
代码如下:
public void MutiTasks() { var t1 = Task.Factory.StartNew<Int32>(() => { Console.WriteLine("任务一执行"); return 1; }); t1.Wait(); var t3 = Task.Factory.StartNew<Int32>(() => { Console.WriteLine("任务三执行"); return t1.Result+3; }); var t4 = Task.Factory.StartNew<Int32>(() => { Console.WriteLine("任务二执行"); return t1.Result + 2; }).ContinueWith<Int32>(task => { Console.WriteLine("任务四执行"); return task.Result + 4; }); Task.WaitAll(t3,t4); Console.WriteLine(t3.Result + t4.Result); }
运行结果:
Task的异常处理
任何应用程序都需要异常处理机制,谁也无法保证自己写得代码在任何时候都是可以正常运行的,那么在Task中到底该怎么处理异常呢?我们先尝试一下用try-catch
public void TaskExcetion() { try { var pTask = Task.Factory.StartNew(() => { var cTask = Task.Factory.StartNew(() => { Thread.Sleep(1000); throw new Exception("子任务异常"); Console.WriteLine("子任务完成"); }); throw new Exception("父任务异常"); Console.WriteLine("父任务完成"); }); pTask.Wait(); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.WriteLine("非关联嵌套"); }
看一下结果:
可见异常信息并不对,其实正确的姿势是这个样子的:
public void TaskAggregateExcetion() { try { var pTask = Task.Factory.StartNew(() => { var cTask = Task.Factory.StartNew(() => { Thread.Sleep(1000); throw new Exception("子任务异常"); Console.WriteLine("子任务完成"); }); throw new Exception("父任务异常"); Console.WriteLine("父任务完成"); }); pTask.Wait(); } catch (AggregateException ex) { foreach (Exception item in ex.InnerExceptions) { Console.WriteLine(item.Message); } } Console.WriteLine("非关联嵌套"); }
运行结果:
这里用了AggregateException,就是异常集合,当然开发中不会只有一个线程,肯定会有多个线程,多个线程就可能有多个异常。当然,除了在task中使用异常,我们还可以通过Task的几个属性来判断Task的状态,如:IsCompleted, IsFaulted, IsCancelled,Exception等等来判断task是否成功的执行了。