关于Task的一点思考和建议
前言
本打算继续写SQL Server系列,接下来应该是死锁了,但是在.NET Core项目中到处都是异步,最近在写一个爬虫用到异步,之前不是很频繁用到异步,当用到时就有点缩手缩尾,怕留下坑,还是小心点才是,于是一发不可收拾,发现还是too young,所以再次查看资料学习下Task,用到时再学效果可想而知,若有不同意见请在评论中指出。
建议异步返回Task或Task<T>
当在.NET Core中写爬虫用到异步去下载资源后接下来进行处理,对于处理完成结果我返回void,想到这里不仅仅一愣,这么到底行不行,翻一翻写的第一篇博客,只是提醒了我下不要用void,至于为何不用也没去探讨,接下来我们来探讨下返回值为Task和void,至于Task<T>这个和Task类似。我们直接看代码,首先演示void,如下:
private static async void ThrowExceptionAsync()
{
await Task.Delay(TimeSpan.FromSeconds());
throw new InvalidOperationException();
}
private static void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
try
{
ThrowExceptionAsync();
}
catch (Exception ex)
{ throw ex;
}
}
然后在控制台中进行调用,如下:
static void Main(string[] args)
{
AsyncVoidExceptions_CannotBeCaughtByCatch();
Console.ReadKey();
}
此时我们在异步代码且返回值为void的方法中有一个异常,并且我们在调用该异步方法中去捕捉异常,但是结果并未捕捉到。接下来我们将异步方法返回值修改为Task如下再来看看:
private static async Task ThrowExceptionAsync()
{
await Task.Delay(TimeSpan.FromSeconds());
throw new InvalidOperationException();
}
private static async Task AsyncVoidExceptions_CannotBeCaughtByCatch()
{
try
{
await ThrowExceptionAsync();
}
catch (Exception ex)
{ throw ex;
}
}
此时发现返回值Task和void对于异常都无法捕捉到,这么一来是不是返回值使用Task和void皆可以呢,我们注意到对于被调用的异步方法且返回值为Task,我们试试将先接收其返回值,然后再await看看。此时我们对于第二个异步方法修改成如下:
private static async Task AsyncVoidExceptions_CannotBeCaughtByCatch()
{
Task task = ThrowExceptionAsync();
try
{
await task;
}
catch (Exception ex)
{ throw ex;
}
}
通过事先接收其返回值Task然后再await,此时我们就能捕捉到异常,而为什么void无法捕捉到异常呢?请看如下解释
当在Task或者Task<T>中抛出异常时,此时异常信息将被捕捉到并被放到Task对象中,但是在void异步方法启动时SynchronizationContext将被激活并且此时没有Task对象,此时异常信息将直接被保存到异步上下文中即(SynchronizationContext)。
对于捕捉void异常信息其实没有什么根本上的解决办法,如果是在控制台中可以用下载 Nito.AsyncEx 程序包并将方法放在 AsyncContext.Run(()=>.....) 运行,还有其他等方法,返回值为void更多用在windows客户端事件处理程序包中,例如如下:
private async void btn_Click(object sender, EventArgs e)
{
await BtnClickAsync();
}
public async Task BtnClickAsync()
{
// Do asynchronous work.
await Task.Delay();
}
在异步操作中如果返回值为Task或者Task<T>,我们知道接下来给如何进行处理,但是返回值为void我们根本不知道它什么时候完成,同时利用void来进行单元测试时也不会抛出异常,所以我们对于异步返回值大部分情况下必须使用Task或者Task<T>,除了基于事件处理而不得不返回void外,对于Task或者Task<T>有利于异常捕捉、暴露更多方法如(Task.WhenAll、Task.WhenAny)、方便单元测试等,基于此我们在此下一个基本结论:
虽然在异步方法中提示返回值可以为Task、Task<T>或者void,但是我们强烈建议返回值只为Task或者Task<T>,除了基于事件处理程序外,因为返回值为void无法捕捉异常信息且不方便单元测试,同时根本不知道异步操作什么时候完成。而对于Task异常信息被保存到Task对象中,所以在捕捉异常信息时,首先返回异步方法Task,然后进行await。
但是对于Task捕捉异常信息还有一个问题我们并未探讨,请往下看。
public static Task<int> First()
{
return Task<int>.Factory.StartNew(() =>
{
throw new Exception(" Exception From First!");
});
}
public static Task<int> Second()
{
return Task<int>.Factory.StartNew(() =>
{
throw new Exception(" Exception From Second!");
});
}
上述定义两个异步方法,并且都抛出异常,接下来我们再来定义一个方法调用上述两个方法,如下:
public static async Task<int> Caclulate()
{
return await First() + await Second();
}
上述情况下理论上调用两个方法应该抛出两个异常信息才对,但是结果只对一个First异步方法抛出异常,而对于第二个异步方法Second则忽略了,什么情况,还没看懂,我们进一步进行如下改造。
static void Main(string[] args)
{
try
{
Caclulate().Wait();
}
catch (AggregateException ex)
{ throw ex;
}
Console.ReadKey();
}
我们通过聚合异常类 AggregateException 来接收异常信息,结果只抛出一个异常信息,并且是第一个。 我们再利用返回Task来接收并await来看看是否有不同。
public static async Task Test()
{
var task = Caclulate();
try
{
await task;
}
catch (Exception ex)
{ throw ex;
}
}
此时也将仅仅抛出第一个异常信息,所以通过这里演示我们可以下个结论:当在异步代码中调用多个异步方法时,若出现异常,此时则不会抛出聚合异常而仅仅只是抛出第一个异常。
建议异步感染
在异步操作中如果异步代码又被其他异步代码调用时,将同步代码转换为异步代码能够更有效执行,在异步代码中没有感染的概念,为什么我提出“感染”这一概念呢,想必正确使用过异步方法的童鞋深有体会,当一个异步方法被另外一个方法调用时,此时另外一个方法若是同步方法,此时会提示将该方法异步,所以通过该传播行为从最底层异步方法到最高层调用者都将是异步方法(类似僵尸尸毒),这也是我们所推荐的,一旦用了异步代码则总是用异步代码,不要将同步代码和异步代码混合使用,很容易导致阻塞情况特别是调用Task.Wait或者Task.Result。这一点我有切身感受,在爬虫中利用同步方法中调用异步代码,最终获取该异步方法中的结果通过Task.Rsult,结果利用Windows窗体测试时发现已经被阻塞,一直显示Task.Result处于计算中。不信,你看如下代码。所以我们强烈建议:一旦使用异步代码且总是使用异步代码让异步代码自然过渡层层传递,大部分情况下千万别调用Task.Wait或者Task.Result很容易导致阻塞。
public static class DeadlockDemo
{
private static async Task DelayAsync()
{
await Task.Delay();
} public static void Test()
{
var delayTask = DelayAsync();
delayTask.Wait();
}
}
private void btn_click(object sender, EventArgs e)
{
DeadlockDemo.Test();
MessageBox.Show("异步死锁");
}
将上述代码在windows form或者ASP.NET程序中运行你会发现上述调用Wait后会导致死锁,但在控制台中将不会出现这种死锁情况。按照我们对异步的理解,默认情况下,当一个未被完成的任务被await时,此时将捕捉到当前上下文,直到任务被完成唤醒该方法,如果当前上下文为空,那么此时当前上下文则为SynchronizationContext。对于如winddows form中的GUI或者ASP.NET应用程序,此时任务调度器的上下文则是SynchronizationContext且只允许一块代码运行一次,当任务完成时,将试图在捕捉的当前上下文去执行异步方法中的其他方法,但是此时已经有一个线程当前上下文存在,造成同步方法去等待完成异步方法,结果引起异步方法唤醒当前方法继续执行,但是当前同步方法也在等待异步方法完成,彼此等待,造成死锁。
建议异步配置上下文(分情况)
什么时候应该配置上下文,当我们需要等待结果完成时可以配置上下文,如下:
async Task ConfigureContext()
{
await Task.Delay(); await Task.Delay().ConfigureAwait(
continueOnCapturedContext: false); }
当进行如上配置后在 await Task.Delay(); 之前毫无疑问将在原始上下文中运行, await Task.Delay().ConfigureAwait( continueOnCapturedContext: false); 此时在此之后因为不捕捉上下文,此时将在线程池中运行。我们在此之前演示了一个造成死锁的例子,通过配置上下文就可以解决。
private static async Task DelayAsync()
{
await Task.Delay().ConfigureAwait(
continueOnCapturedContext: false);
} public static void Test()
{
var delayTask = DelayAsync();
delayTask.Wait();
}
我们知道默认情况下当await一个未完成的任务时,此时将捕获上下文来唤醒异步方法来执行其余的方法,但是此时我们配置上下文为false,告诉它不需要捕获我们根本不耗费时间,我们马上就能完成,此时将解决死锁的问题。在异步中配置 ConfigureAwait( continueOnCapturedContext: false); 的作用在于:将同步方法转换为异步方法和防止死锁。
那么问题来了什么时候不应该配置上下文呢?请继续看如下例子:
private async void btn_click(object sender, EventArgs e)
{
Enabled = false;
try
{
await Task.Delay().ConfigureAwait(
continueOnCapturedContext: false);
}
finally
{ Enabled = true;
} }
当点击按钮时我们禁用按钮,同时关闭了其捕获当前上下文,但是最后我们又需要用到当前上下文,所以此时导致取不到一样的线程,此时类似跨线程,出现线程不一致的情况。每个异步方法都有其上下文并且每个方法的上下文是独立开来的。什么意思呢,由于上述我们直接在点击事件里面关闭了捕获上下文,如果我们定义一个方法,在此方法里面来关闭捕获上下文,此时再来在点击事件里调用该异步方法,此时点击事件和该异步方法独立互不影响,千万别以为调用了该异步方法就说明是在点击事件里关闭了上下文,如下:
private async void btn_click(object sender, EventArgs e)
{
Enabled = false;
try
{
await DisableBtnAsync();
}
finally
{ Enabled = true;
} } private async Task DisableBtnAsync()
{ await Task.Delay().ConfigureAwait(continueOnCapturedContext:
false);
}
由上已经证明了这点,好了本节我们到此结束。
总结
关于异步和Task中的水还是非常深,我也是用到了再去深究,本节算是对异步中的异常捕获以及返回值和配置上下文作了一个大概的探讨。
关于Task的一点思考和建议的更多相关文章
- 关于java异常的一点思考
关于异常的一点思考 异常生命周期 异常的来源 所有的异常都是抛出来的 有底层api抛出的 有自定义抛出的 异常的处理 1, 运行时异常 不做任何处理仍可编译通过 不建议捕获(不建议用异常来做流程控制, ...
- c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程
c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...
- 【翻译】全球用尽IPv4的一点思考
作者:Dimple 公众号:奔跑吧攻城狮 简介:专属于Java和Android开发,和你聊聊职场话题,一同展望未来 作为小小号主的我表示很无力啊,这几天,天天都是热点.前有网易员工勇敢发声维护自己的利 ...
- MSSQL显错注入爆数字型数据的一点思考
Title:MSSQL显错注入爆数字型数据的一点思考 --2011-02-22 15:23 MSSQL+ASP 最近在弄个站点,密码是纯数字的,convert(int,())转换出来不报错,也不知道其 ...
- 对dump脱壳的一点思考
对dump脱壳的一点思考 偶然翻了一下手机日历,原来今天是夏至啊,时间过的真快.ISCC的比赛已经持续了2个多月了,我也跟着比赛的那些题目学了2个月.......虽然过程很辛苦,但感觉还是很幸运的,能 ...
- 关于linux kernel slab内存管理的一点思考
linux kernel 内存管理是个很大的话题,这里记录一点个人关于slab模块的一点思考总结. 有些书把slab介绍成高速缓存,这会让人和cache,特别是cpu cache混淆,造成误解.sla ...
- 关于html页面元素语义化的一点思考
这几天在看招聘公告前端工程师的要求基本都附带了html语义化的要求,所以稍微关注了下这方面的知识.对于其中的一点就是要求页面元素在去除css样式之后还能有良好的布局引发了我一点思考.作为前端刚入门的我 ...
- 基于CAS分析对ABA问题的一点思考
基于CAS分析对ABA问题的一点思考 什么是CAS? 背景 synchronized加锁消耗太大 volatile只保证可见性,不保证原子性 基础 用CPU提供的特殊指令,可以: 自动更新共享数据; ...
- Java生鲜电商平台-源码地址公布与思考和建议
Java生鲜电商平台-源码地址公布与思考和建议 说明:今天是承诺给大家的最后一天,我公布了github地址(QQ群里面有).诚然这个是我的计划中的事情,但是有以下几点思考请大家共勉: 1. 你下了那么 ...
随机推荐
- 安卓 Android题目大全
安卓001个人事务管理系统(单端) 安卓002手机订餐系统 安卓003无线点菜 安卓004酒店房间预定系统 安卓005个人相册管理系统(单端) 安卓006计算器(单端) 安卓007英语学习(单端) ...
- STM32 的加密实现(转)
源:STM32 的加密实现 基于STM32F103的ID号对应用程序的保护方法 目的:对运行于STM32的嵌入式代码程序进行加密 编译环境:IAR Embedded System for ARM5.5 ...
- mrql初级教程-概念、使用(一)
以下是本人原创,如若转载和使用请注明转载地址.本博客信息切勿用于商业,可以个人使用,若喜欢我的博客,请关注我,谢谢!博客地址 感谢您支持我的博客,我的动力是您的支持和关注!如若转载和使用请注明转载地址 ...
- SSL证书指令
转自:http://blog.csdn.net/madding/article/details/26717963 生成Self Signed证书 # 生成一个key,你的私钥,openssl会提示你输 ...
- AFNetworking封装思路简析
http://blog.csdn.net/qq_34101611/article/details/51698473 一.AFNetworking的发展 1. AFN 1.0版本 AFN 的基础部分是 ...
- IOS第三方数据库--FMDB
iOS中原生的SQLite API在使用上相当不友好,在使用时,非常不便.于是,就出现了一系列将SQLite API进行封装的库,例如FMDB.PlausibleDatabase.sqlitepers ...
- make 要点简记
make 要点简记 1.隐式推导 make可以自动推导文件及其文件依赖关系后面的命令,所以我们没有必要在每一个.o文件后面都写上类似的命令,因为make 会自动识别并且自动推导命令. objects ...
- C # 产生鼠标点击事件
新建一个WinFrom,找到MouseDown,回车,生成代码如下点击的效果如图 参考文章:http://blog.csdn.net/u012842807/article/details/454143 ...
- TF-IDF算法 笔记
TF-IDF:Term Frequency-Inverse Document Frequency(词频-逆文档频度):主要用来估计一个词在一个文档中的重要程度. 符号说明: 文档集:D={d1,d2, ...
- 动态创建Fastreport(delphi)
动态创建Fastreport分以下几个步骤: 1.首先清空Fastreport,定义全局变量,并加载数据集 frReport.Clear; frReport.DataSets.Add(fr ...