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

396884fd2784e3566998f712d7738605.png

前言

071356e428a9e6cb94d21ad009b0be65.png

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

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

第三个异步编程模型:TPL

7cc6259e637dd56b4a692e48fb3d6c38.png

概述

35e70a153f58921809fdf3acaaa25283.png

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

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

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

a7ced7a5810e983453ce365e1a6945c9.png

创建TPL

1856e3b6850c0bc1b894274dcaf5f284.png

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

ca98e92e9067e501849d54468afa942c.png

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

在第一种方式中,使用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}");}}
}

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

c3b97ea9c596fe95fa17873502e02201.png

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

8662cf925396e6f3ad4d919a3fb8479c.png

实现取消

2a8df7669db0fd3c70fb6c4e9af2dad7.png

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

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

398eafaed30865e75a3b69f973dbefc0.png

相关代码如下:

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

运行后结果如图所示:

89aac2cf693af46b90e7734cdc6e8139.png

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

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

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

11718effe6c44045022fa4eead6405df.png

处理异常

26a1e0741d91b744f55a0883a8cd36ef.png

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

28c33596d6e2d7ce910665be5d5d6932.png

相关代码如下:

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("异常!");}}
}

运行后结果如图所示:

da88bb9b2d455416ec06e585e723bd17.png

从代码实现和运行结果中,我们可以看出调用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("异常!");}}
}

运行后结果如图所示:

76362648af5d09b8f9c23533ce68e1d2.png

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

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

a4bed193f05bdde317e436f22b4f8d85.png

任务并行

ade659f07763554b4d843f5a87f5dbe2.png

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

14efa35662af5eb6415690b92a1dc0b9.png

我们分别创建了三个任务,但任务之间并不再是无关联的关系,而是使用了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;}}
}

运行后结果如图所示:

c82a78e6190b22c83e46ba9f21a033a1.png

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

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

32031fa0d75a9dbfd3a36822743da314.png

小结

ca6de2a1c1057969efcbdd5ec2fbf581.png

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

030b1c401da9d69cb195028184fbef8a.png

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

10cd4dd06fab34238c0d395c30e84825.png

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

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

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

cd0095331b8bb1d9bc38934241596afe.png

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

40fce8e1fe49ef16a8b374fff1864e82.png

9382cb3d0323859368270f7b1d06d4bd.png

b76e6c2303603f22d7c07774f899d17b.png

公众号:wacky的碎碎念

知乎:wacky

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/296305.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Google:推荐几款好用的Chrome浏览器插件

1、Clear Cache 0.3.3.2 一键清空浏览器缓存数据。 https://chrome.google.com/webstore/detail/clear-cache/cppjkneekbjaeellbfkmgnhonkkjfpdn 2、Pig Toolbox 1.0.6.4 双击关闭页签&#xff0c;鼠标手势&#xff0c;手势动作轮&#xff0c;摇臂&#xff0c;超级拖拽&#xf…

豆瓣8.7!BBC这部成人社会禁片,曝光了行业内不能说的秘密

全世界只有3.14 % 的人关注了爆炸吧知识不知道生活中的你是否也会这样&#xff1f;平时即便不买东西&#xff0c;没事也会打开淘宝看看。但凡遇上双11、618各种促销节&#xff0c;总觉得不买好像就亏了&#xff0c;每每忍不住手痒&#xff0c;交了一堆智商税之后又开始后悔。如…

qt4.7 mysql_详解Qt 4.7编译和访问Mysql驱动

Qt4.7编译和访问Mysql驱动是本文要介绍的内容&#xff0c;不多说&#xff0c;我们来看内容。今天摸索了一上午&#xff0c;终于用qt连上mysql了1、安装一个mysql5.0以上版本***要求&#xff1a;(1)安装路径不要有空格和点(2)不要默认安装&#xff0c;选自定义安装&#xff0c;保…

Lync-用户-电话号码-更新

1. 更新-用户-手机号 2. 服务器-更新-地址簿 3. 客户端-更改-注册表-<只操作一次!> 在命令提示符中输入如下命令&#xff1a; Reg Add HKLM\Software\Policies\Microsoft\Communicator /v GalDownloadInitialDelay /t REG_DWORD /d 0 /f 4. 客户端-删除-用户信息 退出-Ly…

读取数量不定的输入数据

对于整数求和&#xff0c;我们经常用循环来求所求数个数确定的一组数。可是&#xff0c;我们预先不知道要对多少个数求和&#xff0c;这就需要不断读取数据直至没有新的输入为止&#xff1a; #include<iostream> int main() {int sum 0, value 0;// sum为出入数字的和&…

AdBlock屏蔽网易的“我来挑错”和“转发至微博”

今天我的AdBlock出错了&#xff0c;重装了下后发现以前的一些自定义的配置丢了&#xff0c;其中一个比较常用的就是屏蔽网易的的"我来挑错"和"转发至微博"两个按钮&#xff0c;便重新分析了一下&#xff0c;找到了屏蔽方法。这里记录一下&#xff0c;以备后…

豆瓣评分9分+,6部经典趣味数学纪录片!

全世界只有3.14 % 的人关注了爆炸吧知识数学是研究数量、结构、变化以及空间模型等概念的一门学科。透过抽象化和逻辑推理的使用&#xff0c;由计数、计算、量度和对物体形状及运动的观察中产生。数学家们拓展这些概念&#xff0c;为了公式化新的猜想以及从合适选定的公理及定义…

C# Hook原理及EasyHook简易教程

前言在说C# Hook之前&#xff0c;我们先来说说什么是Hook技术。相信大家都接触过外挂&#xff0c;不管是修改游戏客户端的也好&#xff0c;盗取密码的也罢&#xff0c;它们都是如何实现的呢&#xff1f;实际上&#xff0c;Windows平台是基于事件驱动机制的&#xff0c;整个系统…

java当前时间推前三个月_获取当前时间的前三个月 java

java获取当前路径的几种方法 1、利用System.getProperty()函数获取当前路径&#xff1a; System.out.println(System.getProperty("user.dir"));//user.dir指定了当前的路径 2、使用File提供的函数获取当前路径&#xff1a; File directory new File(""…

squid 服务器的应用

实验名称&#xff1a;squid 服务器的应用 实验目标&#xff1a; 任务一&#xff1a;实现正向代理 任务二&#xff1a;实现透明代理 任务三&#xff1a;实现反向代理 提示1、在启动squid服务程序之前需要先确认Linux主机具有完整的域名&#xff0c;如果没有可以在hosts文件中进行…

有的人走着走着就散了!

1 有的人走着走着就走了上坡路▼2 没有感情的甩绳机器▼3 大男孩们陪小朋友踢球到底谁玩的比较开心▼4 给我妈妈演示一下我在肚子里的时候是怎么踹你的▼5 狗子&#xff1a;谢谢您嘞&#xff0c;我这是耳朵不是抹布&#xff01;▼6 从没想到会在这种情况下和你相遇▼7 所…

官宣 .NET 6 RC (Release Candidate) 2

我们很高兴发布 .NET 6 RC(Release Candidate) 2。它是生产环境中支持的两个“go live”候选版本中的第二个。在过去的几个月里&#xff0c;团队一直专注于质量的改进。这个版本中有很多的新特性&#xff0c;但在接近尾声时我们才会把他们完全整合在一起。该团队目前正在验证端…

SQL Server索引进阶第十篇:索引的内部结构

索引设计是数据库设计中比较重要的一个环节&#xff0c;对数据库的性能其中至关重要的作用&#xff0c;但是索引的设计却又不是那么容易的事情&#xff0c;性能也不是那么轻易就获取到的&#xff0c;很多的技术人员因为不恰当的创建索引&#xff0c;最后使得其效果适得其反&…

JDK安装及java环境配置_JDK安装及Java环境变量配置

2.点击Accept License Agreement&#xff0c;下载适合自己电脑版本的JDK.由于我的电脑是windows10 64位专业版。点击红色下载按钮。保存位置自己决定&#xff0c;只要自己安装时能找到就行。3.找到安装文件&#xff0c;双击。4.下一步&#xff0c;这里会让你选择安装目录。注意…

使用 NuGet 管理项目库

共享和重用代码是一个很大的挑战。 不相信&#xff1f; 请随便走进一间中型或大型工作室&#xff0c;问问他们有多少日志记录库。 访问多家公司后&#xff0c;您将发现他们拥有比例非常高的内部日志记录库&#xff0c;而这些库中有一些非常不错&#xff0c;例如&#xff0c;Log…

33张你没看过的酷炫化学动图, 秒懂化学反应原理!

化学的神奇魅力可是不是随便说说的&#xff0c;神奇起来让人叹为观止。下面就让腾远君带领大家看看传说中的37张神图&#xff0c;了解化学之美吧。1 . 硫氰酸汞分解&#xff08;“法老之蛇”&#xff09;原理&#xff1a;硫氰酸汞受热分解&#xff0c;部分产物燃烧。2Hg(SCN)2→…

二分 + 模拟 - Carries

Carries Problems Link Mean: 给你n个数&#xff0c;让你计算这n个数两两组合相加的和进位的次数. analyse: 脑洞题. 首先要知道&#xff1a;对于两个数的第k位相加会进位的条件是&#xff1a;a%(10^k)b%(10^k)>10^k. 想到这一点后就简单了&#xff0c;枚举每一位&#…

Mvc3(1)

摘录于Pro ASP.NET MVC3 Framework一书&#xff1a; 一. Web Form的缺点 1. view state weight: view state 中保存的数据在每次HTTP请求中来回传递&#xff0c;且包含的数据量可以达到很大&#xff0c;导致用户响应时间比较慢&#xff0c;增加了带宽要求 2.页的生命周期相当…

从编译器层面理解C#中的闭包的这个坑!

前言在公众号上看到一篇文章《正确使用和理解C#中的闭包》&#xff0c;里面提到了闭包的一个坑&#xff1a;当捕获的外部变量为for循环的迭代变量时&#xff0c;C#认为变量i是定义在循环体外的。所以&#xff0c;当添加委托集合的for循环执行完时&#xff0c;i的值已经变为3了&…

ssh 与 locale

1 我的 ubuntu 11.10 使用 zh_CN.UTF-8 ,导致连接到 ssh 服务器上显示中文&#xff0c;本来是很方便的&#xff0c;但是最近要监控某些功能&#xff0c;需要ssh 服务器&#xff08;en_US.UTF-8&#xff09;显示英文 &#xff01; locale LANGzh_CN.UTF-8 LANGUAGEzh_CN:en_US:…