关于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(1));
            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(1));
            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(1000);
}

在异步操作中如果返回值为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(1000);
            }
            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(1000);
        }

        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(1000);

            await Task.Delay(1000).ConfigureAwait(
              continueOnCapturedContext: false);

        }

当进行如上配置后在 await Task.Delay(1000); 之前毫无疑问将在原始上下文中运行,  await Task.Delay(1000).ConfigureAwait( continueOnCapturedContext: false); 此时在此之后因为不捕捉上下文,此时将在线程池中运行。我们在此之前演示了一个造成死锁的例子,通过配置上下文就可以解决。

        private static async Task DelayAsync()
        {
            await Task.Delay(1000).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(1000).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(1000).ConfigureAwait(continueOnCapturedContext:
                false);
        }

由上已经证明了这点,好了本节我们到此结束。

总结

关于异步和Task中的水还是非常深,我也是用到了再去深究,本节算是对异步中的异常捕获以及返回值和配置上下文作了一个大概的探讨。

时间: 2024-10-29 01:05:03

关于Task的一点思考和建议的相关文章

关于后台系统自动生成的一点思考

大量实践发现后台管理程序,其实90%的代码都是相同的,当然是在抛弃复杂逻辑业务的情况下,那么如何能高效的节约这些时间呢,那就是接下来我要说的,对于后台系统自动生成的一些思考. 适用情景: 1.表编号id为自增(基于现在大部分表编号都是自增的情况): 2.没有太复杂业务关联关系,比如表的某一个字段,存储了一个json对象,为了平衡后台用户使用,需要友好的分段展示给用户的定制ui界面:还比如表中存储了外键的多个id,但为了方便用户使用,只能已标签name的方式,给用户展示,等等这些超强业务黏合逻辑的

关于前端的一点思考

关于前端的一点思考 Author:tkorays 最近写前端代码,写着写着就突然开始惆怅.忧伤.愤怒.发狂,我TMD到底在干什么啊! 很多东西写了n遍了,但是还是在不停地写着.自己写过的代码也不想再修改完善.重新利用,只是觉得,可能重新写一遍可能要好点.面对这很多库以及框架,虽然喜爱,但是也是有所顾忌,我只要使用其中的一个功能,根本不需要引入这么大的整个库. 事实上,我们可能在动手写任何代码之前,先要思考下,我们到底要的是什么! 0x00 界面真的需要这么炫酷么 在使用某个界面库之前,我们可能先

关于Emit中动态类型TypeBuilder创建类标记的一点思考

  利用TypeBuilder是可以动态创建一个类型,现在有个需求,动态生成一个dll,创建类型EmployeeEx,需要继承原dll里面的Employee类,并包含Employee类上的所有类标记.   网上有很多例子, //创建TypeBuilder. TypeBuilder myTypeBuilder = myModBuilder.DefineType(typeName, TypeAttributes.Public); myTypeBuilder.SetParent(type);   大概

关于失败的一点思考

睡觉之前突然想到马云说过的一句话:我们要习惯于拒绝,习惯失败,如果我们还没成功,那是因为我们的失败还不够 --------2016.4,11  以此自勉 关于失败的一点思考

有关盒模型的一点思考

有关盒模型的一点思考 盒子模型是css中一个重要的概念,理解了盒子模型才能更好的排版. 其实盒子模型有两种,分别是标准 w3c 盒子模型和 IE 盒子模型. 他们对盒子模型的解释各不相同,先来看看我们熟知的标准盒子模型: 一.w3c盒子模型 看下面的图,根据色块,右外倒内,分别代表margin.border.padding.content(即网页内容部分) 二.IE盒子模型 与w3c盒子模型的组成部分类似,IE盒子模型也包括上图几个部分 但是不同的是,IE盒子模型把border和padding归

关于模板方法和策略模式的一点思考

该随笔的思想原点,应该算是在两三年前了.当时和一前同事聊天.不知怎得就聊到了Http访问. 一.我记得他和我说过的第一句话,大概是:有没有已经封装好的.比较强大的HttpUtil.也可能是受业务的影响(接口对内).我当时接触到的Http访问,大多比较“规范”,至少有一个接口约束在约定着某些东西,不至于一会传递json,返回json, 一会又要传递xml,返回xml,甚至更奇葩的是,上传个文件.返回0或者1.如果真出现这样的状态,HttpUtil依然能够方便.灵活的适应着各种情况.我想这个Util

关于android SDK安装Failed to fetch URL 一点思考

最近SDK出问题了,然后在google下载了一个android-sdk-windows.rar,然后点击SDK Manager,结果一直不能刷新API Level,然后就开始在网上找了好多资料,解决这个问题,修改 HOSTS,    HTTP  和  HTTPS  都不能解决,这给我带来了很大的困惑!   加载不出来的界面错误为: Fetching http://dl-ssl.google.com/android/repository/addons_list-1.xml Failed to fe

JavaScript组合继承的一点思考

今天看<JavaScript高级程序设计>一书中关于组合继承模式时,书上有这么一个Demo程序: <html> <head> </head> <body> <script> function SuperType(name){ this.name = name; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name,

关于工作习惯的一点思考

最近项目发布新的版本,一个月要求四个人完工上线.我负责实现接口和相应的数据处理,从整体的任务比重上看能站到20%左右.我平时做事情比较赶,也就是属于拿的活差不多有个大体了解,就开始干,到功能实现为止.所谓的功能实现,就是能拿到相应的数据,至于数据整不正确,我一点兴趣都不感.所以整个项目下来,当别人在忙着写前端实现的时候,我就开始闲了,能到别人去调我的方法的时候,才发现我的方法,这里少个判断,那里数据错位... 昨天客户要求在下班前发布新版本,并且把老版本的用户数据同步到新版本上,由于数据结构做了