前言

我们在此前已经介绍了APM模型和EAP模型,以及它们的优缺点。在EAP模型中,可以实时得知异步操作的进度,以及支持取消操作。但是组合多个异步操作仍需大量工作,编写大量代码方可完成。

因此,在.Net Framework 4.0中,引入了一个新的关于异步操作的模型,叫做任务并行库,简称为TPL。

第三个异步编程模型:TPL

概述

TPL,全称为Task Parallel Library,它可以被认为是线程池之上的又一个抽象层,隐藏了部分底层细节,核心概念为任务。

一个任务代表了一个异步操作,该操作可以通过多种方式运行,可以使用或者不使用独立线程(如Thread)运行,还可以通过多种方式和其他任务组合起来。

在本文中,我们将探究TPL的使用方式,以及如何正确处理异常,取消任务,如何使多个任务同时执行等。

创建TPL

我们首先需要创建一个控制台程序,用来执行Task的创建和运行,并在Task内部使用委托调用一个方法,用来打印当前任务以及当前任务所在的线程信息,如图所示:

我们分别使用了三种方式来创建任务并执行:

在第一种方式中,使用new Task类的方式,把需要执行的内容放入Action委托并传入参数,最后使用Start方法开启任务执行,若不调用Start方法,则不会启动任务,切记。

在第二种方式和第三种方式中,被创建的任务会立即开始工作,所以无需显式调用Start方法。Task.Run与Task.Factory.StartNew的区别为,前者是后者的一个快捷方式,但后者拥有附加选项,如没有特殊需求,通常使用前者来创建任务。

相关代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace TPLDemo
{class Program{static void Main(string[] args){var t1 = new Task(() => TaskMethod("任务1"));var t2 = new Task(() => TaskMethod("任务2"));t1.Start();t2.Start();Task.Run(() => TaskMethod("任务3"));Task.Factory.StartNew(() => TaskMethod("任务4"));Task.Factory.StartNew(() => TaskMethod("任务5"), TaskCreationOptions.LongRunning);Thread.Sleep(TimeSpan.FromSeconds(1000));}/// <summary>/// 任务运行的方法/// </summary>/// <param name="name">The name.</param>static void TaskMethod(string name){Console.WriteLine($@"Task {name} 是一个正在线程id为 {Thread.CurrentThread.ManagedThreadId} 上运行的任务,是否为线程池线程:{Thread.CurrentThread.IsThreadPoolThread}");}}
}

接着我们来看一下运行结果,如图所示:

可以看出任务1,2,3,4均为线程池中的线程,也印证了我们此前的概念,TPL为线程池上的一个抽象层。而任务5在实现时被我们标记为需要长时间运行的任务,因此在调度时,并未使用线程池中的线程,而是单独开启一个线程执行,这样可以避免线程池中的线程被长时间占用,无法复用资源。

实现取消

在EAP模型中,我们借助BackgroundWorker组件封装好的取消方法,可以对正在执行的线程进行取消。那么这样的方式毕竟是有很大的局限性的,因此,在Net Framework 4.0中,微软创建了统一的模型来协作取消涉及两个对象的异步操作或长时间运行的同步操作,它就是CancellationTokenSource和CancellationToken。

我们需要创建CancellationTokenSource实例以传入Task,来标识此任务包含外部取消操作,然后使用CancellationToken来传播任务内的应取消操作的通知,如图所示:

相关代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace TPLDemo
{class Program{static void Main(string[] args){var cts = new CancellationTokenSource();var longTask = new Task<int>(() => TaskMethod("任务1", 10, cts.Token), cts.Token);Console.WriteLine(longTask.Status);cts.Cancel();Console.WriteLine(longTask.Status);Console.WriteLine("任务1在执行前已经被取消");cts = new CancellationTokenSource();longTask = new Task<int>(() => TaskMethod("任务2", 10, cts.Token), cts.Token);longTask.Start();for (int i = 0; i < 5; i++){Thread.Sleep(TimeSpan.FromSeconds(0.5));Console.WriteLine(longTask.Status);}cts.Cancel();for (int i = 0; i < 5; i++){Thread.Sleep(TimeSpan.FromSeconds(0.5));Console.WriteLine(longTask.Status);}Console.WriteLine($"任务2执行完成,结果:{longTask.Result}");Console.Read();}/// <summary>/// 任务取消的方法/// </summary>/// <param name="name"></param>/// <param name="seconds"></param>/// <param name="token"></param>/// <returns></returns>private static int TaskMethod(string name, int seconds, CancellationToken token){Console.WriteLine($@"Task {name} 是一个正在线程id为 {Thread.CurrentThread.ManagedThreadId} 上运行的任务,是否为线程池线程:{Thread.CurrentThread.IsThreadPoolThread}");for (int i = 0; i < seconds; i++){Thread.Sleep(TimeSpan.FromSeconds(1));if (token.IsCancellationRequested){return -1;}}return 42 * seconds;}}
}

运行后结果如图所示:

从代码中,我们可以看出,我们给Task传递了两次CancellationTokenSource,一次是任务内执行方法,一次是任务本身构造函数,那么为什么要这样做呢?

因为如果我们在任务启动之前进行取消,那么该任务所在的TPL模型,就会“接管”该取消操作,因为这些代码根本不会继续执行。我们查看第一个任务的状态可以得知,它已经被取消了,如果在此时再调用Start方法,那么将会抛出一个异常。

而在第二个任务中,我们先执行任务,再做取消,那么此时我们相当于是在外部对此任务进行取消控制,而且在执行取消之后,任务2的状态依然是RanToCompletion,而不是Canceled。因为从TPL的角度来看,该任务正常完成了它的工作,所以我们在编写代码时需要辨别这两种情况,同时理解它在两种情况下职责的不同。

处理异常

在普通情况下,我们通常使用try-catch代码块来处理异常,但在TPL中,最底层的异常会被封装为一个AggregateException的通用异常,如果需要获取真正的异常,则需要访问InnerException属性,相关实现如图所示:

相关代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace TPLDemo
{class Program{static void Main(string[] args){Task<int> task;try{task = Task.Run(() => TaskMethod("任务1", 2));int result = task.Result;Console.WriteLine($"结果为:{result}");}catch (Exception ex){Console.WriteLine($"发生异常:{ex}");}Console.WriteLine("----------------------------------------------------------------------------------------");Console.Read();}static int TaskMethod(string name, int seconds){Console.WriteLine($@"Task {name} 是一个正在线程id为 {Thread.CurrentThread.ManagedThreadId} 上运行的任务,是否为线程池线程:{Thread.CurrentThread.IsThreadPoolThread}");Thread.Sleep(TimeSpan.FromSeconds(seconds));throw new Exception("异常!");}}
}

运行后结果如图所示:

从代码实现和运行结果中,我们可以看出调用Task的Result属性,会使得当前线程等待直到该任务完成,并将异常传播到当前线程,因此我们可以通过catch捕获到该异常,且该异常的类型为AggregateException,同时我们打印出的结果包含底层真正异常内容。

但在TPL中,还有另外一种方式来处理异常,那就是使用Task的GetAwaiter和GetResult方法来获取结果,相关代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace TPLDemo
{class Program{static void Main(string[] args){Task<int> task;try{task = Task.Run(() => TaskMethod("任务1", 2));int result = task.Result;Console.WriteLine($"结果为:{result}");}catch (Exception ex){Console.WriteLine($"发生异常:{ex}");}Console.WriteLine("----------------------------------------------------------------------------------------");Console.WriteLine();try{task = Task.Run(() => TaskMethod("任务2", 2));int result = task.GetAwaiter().GetResult();Console.WriteLine($"结果为:{result}");}catch (Exception ex){Console.WriteLine($"发生异常:{ex}");}Console.WriteLine("----------------------------------------------------------------------------------------");Console.Read();}static int TaskMethod(string name, int seconds){Console.WriteLine($@"Task {name} 是一个正在线程id为 {Thread.CurrentThread.ManagedThreadId} 上运行的任务,是否为线程池线程:{Thread.CurrentThread.IsThreadPoolThread}");Thread.Sleep(TimeSpan.FromSeconds(seconds));throw new Exception("异常!");}}
}

运行后结果如图所示:

我们从结果中可以看出,在这种情况下,可以直接捕获到底层异常,而无需再访问InnerException属性,原因是TPL模型会直接提取该异常进行处理。

由上述两种情况我们可以得出结论:如果你需要直接获取并处理底层异常,那么请使用GetAwaiter和GetResult方法来获取Task的结果,反之,则可直接使用Result属性。

任务并行

我们在之前的示例中,都是单独创建任务并执行,每个任务的执行过程和结果都是独立的。那么,如果我们需要多个任务并行,要怎么做呢?可以使用如下方式:

我们分别创建了三个任务,但任务之间并不再是无关联的关系,而是使用了Task.WhenAll与ContineWith来使得它们以某种方式关联起来。

相关代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace TPLDemo
{class Program{static void Main(string[] args){var firstTask = new Task<int>(() => TaskMethod("任务1", 3));var secondTask = new Task<int>(() => TaskMethod("任务2", 2));var whenAllTask = Task.WhenAll(firstTask, secondTask);whenAllTask.ContinueWith(x =>{Console.WriteLine($"任务1结果为:{x.Result[0]},任务2结果为:{x.Result[1]}");}, TaskContinuationOptions.OnlyOnRanToCompletion);firstTask.Start();secondTask.Start();Console.Read();}static int TaskMethod(string name, int seconds){Console.WriteLine($@"Task {name} 是一个正在线程id为 {Thread.CurrentThread.ManagedThreadId} 上运行的任务,是否为线程池线程:{Thread.CurrentThread.IsThreadPoolThread}");Thread.Sleep(TimeSpan.FromSeconds(seconds));return 42 * seconds;}}
}

运行后结果如图所示:

分析代码及运行结果,我们可以得知,在前两个任务完成后,第三个任务才开始运行,并且该任务的结果提供了一个结果数组,第一个元素是第一个任务的结果,第二个元素是第二个任务的结果,以此类推。

在TPL中,我们也可以创建另外一系列任务,并使用Task.WhenAny的方式等待这些任务中的任何一个执行完成。当有一个任务完成时,会从列表中移除该任务并继续等待其他任务完成,直到列表为空为止。获取任务的完成进展情况,或在运行任务时使用超时,都可以使用Task.WhenAny方法。例如我们等待一组任务运行,并且使用其中一个任务来记录是否超时,如果该任务先完成,那么我们只需取消其他还未完成的任务即可。

小结

我们在这一篇中,讲解了TPL的发展历程和使用方式,对比APM和EAP模型,TPL显得比较灵活且功能强大,支持取消、异常和并行等操作。

但TPL模型仍有它的不足之处

  • 阅读此类程序代码时,仍难以理解程序的实际执行顺序。

  • 处理异常时,不得不使用单独的后续操作任务来处理在之前的异步操作中发生的错误,导致了代码比较分散,增加了复杂度。

所以为了解决这些问题,微软直接从语言层面引入了更高级别的抽象,真正简化了异步编程,使得编写异步程序更为容易。那么它又是什么呢?它能为我们提供多少便利性呢?预知后事如何,且听下回分解。

您的点赞和在看是我创作的最大动力,感谢支持

公众号:wacky的碎碎念

知乎:wacky

浅谈.Net异步编程的前世今生----TPL篇相关推荐

  1. 浅谈.Net异步编程的前世今生----APM篇

    前言 在.Net程序开发过程中,我们经常会遇到如下场景: 编写WinForm程序客户端,需要查询数据库获取数据,于是我们根据需求写好了代码后,点击查询,发现界面卡死,无法响应.经过调试,发现查询数据库 ...

  2. 浅谈.Net异步编程的前世今生----EAP篇

    前言 在上一篇博文中,我们提到了APM模型实现异步编程的模式,通过使用APM模型,可以简化.Net中编写异步程序的方式,但APM模型本身依然存在一些缺点,如无法得知操作进度,不能取消异步操作等. 针对 ...

  3. 浅谈.Net异步编程的前世今生----异步函数篇(完结)

    前言 上一篇我们着重讲解了TPL任务并行库,可以看出TPL已经很符合现代API的特性:简洁易用.但它的不足之处在于,使用者难以理解程序的实际执行顺序. 为了解决这些问题,在C# 5.0中,引入了新的语 ...

  4. java 异步_浅谈Java异步编程

    本文来自网易云社区. Java异步编程引言 Java的异步编程其实是一个充分利用计算机CPU资源,不想让主程序阻塞在某个长时间运行的任务上,这类耗时的任务可以是IO操作.远程调用以及高密度计算任务.如 ...

  5. python编写函数_浅谈Python 函数式编程

    匿名函数lambda表达式 什么是匿名函数? 匿名函数,顾名思义就是没有名字的函数,在程序中不用使用 def 进行定义,可以直接使用 lambda 关键字编写简单的代码逻辑.lambda 本质上是一个 ...

  6. python采用函数编程模式_浅谈Python 函数式编程

    匿名函数lambda表达式 什么是匿名函数? 匿名函数,顾名思义就是没有名字的函数,在程序中不用使用 def 进行定义,可以直接使用 lambda 关键字编写简单的代码逻辑.lambda 本质上是一个 ...

  7. python采用函数式编程模式-浅谈Python 函数式编程

    匿名函数lambda表达式 什么是匿名函数? 匿名函数,顾名思义就是没有名字的函数,在程序中不用使用 def 进行定义,可以直接使用 lambda 关键字编写简单的代码逻辑.lambda 本质上是一个 ...

  8. 浅谈对java编程思想的理解

    浅谈对java编程思想的理解 刚从学校毕业的时候,对于这种概念的理解少之又少 ,只是单纯的从事编码工作,理解也只是停留在对java基本概念的使用上,很局限!随后工作了快三年的时间里,自己不断的理解这种 ...

  9. 【浅墨Unity3D Shader编程】之四 热带雨林篇: 剔除、深度测试、Alpha测试以及基本雾效合辑

    本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接: http://blog.csdn.net/poem_qianmo/article/details/41923661 作者:毛星云(浅墨) ...

最新文章

  1. 02 使用百度地图获得当前位置的经纬度
  2. 刷题总结——生日礼物(bzoj1293单调队列)
  3. iOS 通过Jenkins 自动构建ipa
  4. mysql dump 数据时间_使用mysqldump备份数据及做时间点还原测试步骤
  5. 网站从无到有--个人(第一次)搭建。服务器环境搭建 对大家有帮助那就最好-第一次发...
  6. 电商业务设计:社交电商产品核心设计
  7. 《转》微商48种加人方法
  8. 计算机电池电源转换,整套解决方案:笔记本电脑的外部电源和电池如何实现无缝切换?...
  9. 使用stylebook制作精美界面的方法(firemonkey)
  10. 老路《用得上的商学课》学习笔记(1-5课)
  11. 2015第19周五《象与骑象人》《瞬变》读书笔记
  12. 天才小毒妃 第917章 深藏不露大财主
  13. 【致敬雷神】星星之火,可以燎原
  14. 《算法导论》:跳跃表(Skip List)
  15. iOS 9适配技巧(更新版)
  16. GPU架构与管线总结
  17. 嵌入式软考备考_8 软件测试
  18. 串口的使用–蓝牙模块
  19. Matlab/simulink通信系统仿真入门操作
  20. matlab中cla和clf作用,28377中CLA初始化部分不明白Cla1Task1和cla1Isr1的区别

热门文章

  1. ubuntu12.04 使用gedit 打开txt文件中文乱码的处理方法
  2. 前台jsp页面向后台传汉字出现乱码问题解决办法
  3. backgroundworder 简单使用
  4. 2010年5月系统集成项目管理工程师上午试卷参考答案(讨论版)
  5. linux网卡固件名,修改CentOS7网卡名称为传统名称eth0格式
  6. python判断字符大小写转换_Python 字符串大小写转换的简单实例
  7. python调用api应用接口_Python接口测试之urllib2库应用
  8. ElasticSearch教程——自定义分词器(转学习使用)
  9. 行内元素中去掉文字的上下间距,使得文字所在元素的高度同字体高度一致的方法...
  10. 分析windows宿主机Ping不通linux虚拟机的其中一种情况