Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 543|回复: 0

.NET异步操作学习之一:Async/Await中异常的处理

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-7-15 14:37:46 | 显示全部楼层 |阅读模式

    以前的异常处理,习惯了过程式的把出现的异常全部捕捉一遍,然后再进行处理。Async/Await关键字出来之后的确简化了异步编程,但也带来了一些问题。接下来自己将对这对关键字进行学习。然后把研究结果放在这里。

    本篇中采用MSDN中的一个列子进行学习,链接如下:

    http://msdn.microsoft.com/zh-cn/library/jj619227.aspx

    现在正式开始,首先尝试一个过程式捕捉异步操作中错误的例子:

    class Program
        {
            static async Task ThrowAfter(int timeout, Exception ex)
            {
                await Task.Delay(timeout);
                throw ex;
            }
    
            static void PrintException(Exception ex)
            {
                Console.WriteLine("时间:{0}\n{1}\n======", stopWatch.Elapsed, ex);
            }
    
            private static readonly Stopwatch stopWatch = new Stopwatch();
            static async Task MissHandling()
            {
                var taskOne = ThrowAfter(1000, new NotSupportedException("Error 1"));
                var taskTwo = ThrowAfter(2000, new NotImplementedException("Error 2"));
                try
                {
                    await taskOne;
                }
                catch (NotSupportedException ex)
                {
                    PrintException(ex);
                }
                catch (NotImplementedException ex)
                {
                    PrintException(ex);
                }
            }
            static void Main(string[] args)
            {
                stopWatch.Start();
                MissHandling();
                Console.ReadLine();
            }
        }

    以下是运行结果:

    image_thumb[2]

    从运行结果中可以看出,运行并没有捕捉到所有的错误,Error 2去哪里了?Error 1在1s时即运行并捕捉到了,第二个返回呢?

    C#的async/await功能基于TPL的Task对象,每个await操作符都是“等待”一个Task完成。在之前(或者说如今)的TPL中,Task对象的析构函数会查看它的Exception对象有没有被“访问”过,如果没有,且Task对象出现了异常,则会抛出这个异常,最终导致的结果往往便是进程退出。因此,我们必须小心翼翼地处理每一个Task对象的错误,不得遗漏。在.NET 4.5中这个行为被改变了,对于任何没有被检查过的异常,便会触发TaskSchedular.UnobservedTaskException事件——如果您不监听这个事件,未捕获的异常也就这么无影无踪了。

    按照上面所述,我们对Main()做2个改造:

    1、响应TaskSchedular.UnobservedTaskException:当出错的任务的未观察到的异常将要触发异常升级策略时发生,默认情况下,这将终止进程。

    2、不断地触发垃圾回收,以便Finalizer线程调用析构函数。

    static void Main(string[] args)
            {
                TaskScheduler.UnobservedTaskException += (s, ev) => PrintException(ev.Exception);
                stopWatch.Start();
                MissHandling();
                
                while (true)
                {
                    Thread.Sleep(1000);
                    GC.Collect();
                }
            }

    运行结果中增加了一些信息,如下:

    image_thumb[9]

    从上面的信息中可以看出,UnobservedTaskException事件并非在“抛出”异常后便立即触发,而是在某次垃圾收集过程,从Finalizer线程里触发并执行。从中也不难得出这样的结论:便是该事件的响应方法不能过于耗时,更加不能阻塞,否则便会对程序性能造成灾难性的影响。

    那么,我们如何处理taskOne与taskTwo中出现的2个错误呢?此时,Task.WhenAll登场了。

    增加一个方法:

    static async Task BothHandled()
            {
                var taskOne = ThrowAfter(1000,new NotSupportedException("Error 1"));
                var taskTwo = ThrowAfter(2000,new NotImplementedException("Error 2"));
                try
                {
                    await Task.WhenAll(taskOne, taskTwo);
                }
                catch (NotSupportedException ex)
                {
                    PrintException(ex);
                }
            }

    输出结果:

    image_thumb[12]

    如果您执行这段代码,会发现其输出与第一段代码相同,但其实不同的是,第一段代码中t2的异常被“遗漏”了,而目前这段代码t1和t2的异常都被捕获了,只不过await语句仅仅“抛出”了“其中一个”异常而已。

    WhenAll是一个辅助方法,它的输入是n个Task对象,输出则是个返回它们的结果数组的Task对象。新的Task对象会在所有输入全部“结束”后才完成。在这里“结束”的意思包括成功和失败(取消也是失败的一种,即抛出了OperationCanceledException)。换句话说,假如这n个输入中的某个Task对象很快便失败了,也必须等待其他所有输入对象成功或是失败之后,新的Task对象才算完成。而新的Task对象完成后又可能会有两种表现:

    所有输入Task对象都成功了:则返回它们的结果数组。
    至少一个输入Task对象失败了:则抛出“其中一个”异常。

    全部成功的情况自不必说,那么在失败的情况下,什么叫做抛出“其中一个”异常?如果我们要处理所有抛出的异常该怎么办?接着我们来详细讨论await操作在异常分派时的相关行为。

    Await抛出异常时的行为

    要理解await的行为,还是从理解Task对象的异常表现开始。Task对象有一个Exception属性,类型为AggregateException,在执行成功的情况下该属性返回null,否则便包含了“所有”出错的对象。既然是AggregateException,则意为着可能包含多个子异常,这种情况往往会在任务的父子关系中出现,具体情况可以参考MSDN中的相关说明。在许多情况下一个Task内部只会出现一个异常,此时这个AggregateException的InnerExceptions属性自然也就只一个元素。

    Task对象本身还有一个Wait方法,它会阻塞当前执行代码,直到任务完成。在出现异常的时候,它会将自身的AggregateException抛出:

    try
    {
        t.Wait();
    }
    catch (AggregateException ex)
    {
        ...
    }

    Wait方法是“真阻塞”,而await操作则是使用阻塞语义的代码实现非阻塞的效果,这个区别一定要分清。与Wait方法不同的是,await操作符效果并非是“抛出”Task对象上的Exception属性,而只是抛出这个AggregateException对象上的“其中一个”元素。我向C#开发组询问这么做的设计考虑,他们回答道,这个决策在内部也经历了激烈的争论,最终的选择这种方式而不是直接抛出Task对象上的AggregateException是为了避免编写出冗余的代码,并让代码与传统同步编程习惯更为接近。

    他们举了一个简单的示例,假如一个Task对象t可能抛出两种异常,现在的错误捕获方式为:

    try
    {
        await t1;
    }
    catch (NotSupportedException ex)
    {
        ...
    }
    catch (NotImplementedException ex)
    {
        ...
    }
    catch (Exception ex)
    {
        ...
    }

    显然前者更贴近传统的同步编程习惯。但是问题在于,如果这个Task中包含了多个异常怎么办?之前的描述是抛出“其中一个”异常,对于开发者来说,“其中一个”这种模糊的说法自然无法令人满意,但事实的确如此。从内部邮件列表中的讨论来看,C#开发团队提到他们“故意”不提供文档说明究竟会抛出哪个异常,因为他们并不想做出这方面的约束,因为这部分行为一旦写入文档,便成为一个规定和限制,为了类库的兼容性今后也无法对此做出修改。

    他们也提到,如果单论目前的实现,await操作会从Task.Exception.InnerExceptions集合中挑出第一个异常,并对外“抛出”,这是System.Runtime.CompilerServices.TaskAwaiter类中定义的行为。但是既然这并非是“文档化”的固定行为,开发人员也尽量不要依赖这点。

    WhenAll的异常汇总方式

    其实这个话题跟async/await的行为没有任何联系,WhenAll返回的是普通的Task对象,TaskAwaiter也丝毫不关心当前等待的Task对象是否来自于WhenAll,不过既然WhenAll是最常用的辅助方法之一,也顺便将其讲清楚吧。

    WhenAll得到Task对象,其结果是用数组存放的所有子Task的结果,而在出现异常时,其Exception属性返回的AggregateException集合会包含所有子Task中抛出的异常。请注意,每个子Task中抛出的异常将会存放在它自身的AggregateException集合中,WhenAll返回的Task对象将会“按顺序”收集各个AggregateException集合中的元素,而并非收集每个AggregateException对象。

    我们使用一个简单的例子来理解这点:

    Task all = null;
    try
    {
        await (all = Task.WhenAll(
            Task.WhenAll(
                ThrowAfter(3000, new Exception("Ex3")),
                ThrowAfter(1000, new Exception("Ex1"))),
            ThrowAfter(2000, new Exception("Ex2"))));
    }
    catch (Exception ex)
    {
        ...
    }

    这段代码使用了嵌套的WhenAll方法,总共会出现三个异常,按其抛出的时机排序,其顺序为Ex1,Ex2及Ex3。那么请问:

    • catch语句捕获的异常是哪个?
    • all.Exception这个AggregateException集合中异常按顺序是哪些?

    结果如下:

    • catch语句捕获的异常是Ex3,因为它是all.Exception这个AggregateException集合中的第一个元素,但还是请牢记这点,这只是当前TaskAwaiter所实现的行为,而并非是由文档规定的结果。
    • all.Exception这个AggregateException集合中异常有三个,按顺序是Ex3,Ex1和Ex2。WhenAll得到的Task对象,是根据输入的Task对象顺序来决定自身AggreagteException集合中异常对象的存放顺序。这个顺序跟异常的抛出时机没有任何关系。

    这里我们也顺便可以得知,如果您不想捕获AggregateException集合中的“其中一个”异常,而是想处理所有异常的话,也可以写这样的代码:

    Task all = null;
    try
    {
        await (all = Task.WhenAll(
            ThrowAfter(1000, new Exception("Ex1")),
            ThrowAfter(2000, new Exception("Ex2"))));
    }
    catch
    {
        foreach (var ex in all.Exception.InnerExceptions)
        {
            ...
        }
    }

    当然,这里使用Task.WhenAll作为示例,是因为这个Task对象可以明确包含多个异常,但并非只有Task.WhenAll返回的Task对象才可能包含多个异常,例如Task对象在创建时指定了父子关系,也会让父任务里包含各个子任务里出现的异常。

    假如异常未被捕获

    最后再来看一个简单的问题,我们一直在关注一个async方法中“捕获”异常的行为,假如异常没有成功捕获,直接对外抛出的时候,对任务本身的有什么影响呢?且看这个示例:

    static async Task SomeTask()
    {
        try
        {
            await Task.WhenAll(
                ThrowAfter(2000, new NotSupportedException("Ex1")),
                ThrowAfter(1000, new NotImplementedException("Ex2")));
        }
        catch (NotImplementedException) { }
    }
    
    static void Main(string[] args)
    {
        _watch.Start();
    
        SomeTask().ContinueWith(t => PrintException(t.Exception));
    
        Console.ReadLine();
    }

    本例运行结果如下:

    image_thumb[15]

    AggregateException的打印内容不那么容易读,我们可以关注它Inner Exception #0这样的信息。从时间上说,Ex2先于Ex1抛出,而catch的目标是NotImplementedException。但从之前的描述我们可以知道,WhenAll返回的Task内部的异常集合,与各异常抛出的时机没有关系,因此await操作符抛出的是Ex1,是NotSupportedException,而它不会被catch到,因此SomeTask返回的Task对象也会包含这个异常——也仅仅是抛出这个异常,而Ex2对于外部就不可见了。

    如果您想在外部处理所有的异常,则可以这样:

    Task all = null;
    try
    {
        await (all = Task.WhenAll(
            ThrowAfter(2000, new NotSupportedException("Ex1")),
            ThrowAfter(1000, new NotImplementedException("Ex2"))));
    }
    catch
    {
        throw all.Exception;
    }

    此时打印的结果便是一个AggregateException包含着另一个AggregateException,其中包含了Ex1和Ex2。为了“解开”这种嵌套关系,AggregateException也提供了一个Flatten方法,可以将这种嵌套完全“铺平”,例如:

    SomeTask().ContinueWith(t => PrintException(t.Exception.Flatten()));

    此时打印的结果便直接是一个AggregateException包含着Ex1与Ex2了。

    本例所有代码如下:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Diagnostics;
    using System.Threading;
    namespace AsyncAwaitTest
    {
        class Program
        {
            static async Task ThrowAfter(int timeout, Exception ex)
            {
                await Task.Delay(timeout);
                throw ex;
            }
    
            static void PrintException(Exception ex)
            {
                Console.WriteLine("时间:{0}\n{1}\n======", stopWatch.Elapsed, ex);
            }
    
            private static readonly Stopwatch stopWatch = new Stopwatch();
            static async Task MissHandling()
            {
                var taskOne = ThrowAfter(1000, new NotSupportedException("Error 1"));
                var taskTwo = ThrowAfter(2000, new NotImplementedException("Error 2"));
                try
                {
                    await taskOne;
                }
                catch (NotSupportedException ex)
                {
                    PrintException(ex);
                }
                catch (NotImplementedException ex)
                {
                    PrintException(ex);
                }
            }
            static async Task BothHandled()
            {
                var taskOne = ThrowAfter(1000,new NotSupportedException("Error 1"));
                var taskTwo = ThrowAfter(2000,new NotImplementedException("Error 2"));
                try
                {
                    await Task.WhenAll(taskOne, taskTwo);
                }
                catch (NotSupportedException ex)
                {
                    PrintException(ex);
                }
            }
            static async Task SomeTask()
            {
                Task all = null;
                try
                {
                    await
                        (all=Task.WhenAll(ThrowAfter(1000, new NotSupportedException("Error 1")),
                                     ThrowAfter(2000, new NotImplementedException("Error 2"))));
                    
                }
                catch
                {
                    throw all.Exception;
                }
            }
            static void Main(string[] args)
            {
                
                stopWatch.Start();
                SomeTask().ContinueWith(t => PrintException(t.Exception.Flatten()));
    
                Console.ReadLine();
    
            }
        }
    }
    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-14 03:32 , Processed in 0.077758 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表