博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
15.3 Task 异常
阅读量:4672 次
发布时间:2019-06-09

本文共 9709 字,大约阅读时间需要 32 分钟。

1. 在等待时拆包异常

在等待任务时,任务出错或取消都将抛出异常,但并不是 AggregateException 。大多情 况下为方便起见,抛出的是 AggregateException 中的第一个异常,往往这就是我们想要的。 异步特性就是像编写同步代码那样编写异步代码,如下所示:

1         async Task
FetchFirstSuccessfulAsync(IEnumerable
urls) 2 { 3 //验证url 4 foreach (string url in urls) 5 { 6 try 7 { 8 using (var client=new HttpClient()) 9 {10 return await client.GetStringAsync(url);11 }12 }13 catch (System.Net.WebException)14 {15 //记录日志16 }17 }18 throw new System.Net.WebException("No urls Success");19 }

  目前,先不要在意损失所有的原始异常,以及按顺序获取所有页面。我想说明的是,我们希 望在这里捕获 WebException 。执行一个使用 HttpClient 的异步操作,失败后可抛出 WebException 。我们想捕获并处理它,对吧?但 GetStringAsync() 方法不能为服务器超时等 错误抛出 WebException ,因为方法仅仅启动了操作。它只能返回一个包含 WebException 的任 务 。 如 果 简 单 地 调 用 该 任 务 的 Wait() 方 法 , 将 会 抛 出 一 个 包 含 WebException 的 AggregateException 。任务awaiter的 GetResult 方法将抛出 WebException ,并被以上代码 所捕获。   

当然,这样会丢失信息。如果错误的任务中包含多个异常,则 GetResult 只能抛出其中的一 个异常(即第一个)。你可能需要重写以上代码,这样在发生错误时,调用者就可捕获 AggregateException 并检查所有失败的原因。重要的是,一些框架方法(如 Task.WhenAll() ) 也可以实现这一点。 WhenAll() 方法可异步等待(方法调用中指定的)多个任务的完成。如果其 中有失败的,则结果即为失败,并包含所有错误任务中的异常。但如果只是等待(await) WhenAll() 返回的任务,则只能看到第一个异常。   

幸好,要解决这个问题并不需要太多的工作。我们可以使用可等待模式的知识,编写一个 Task<T> 的扩展方法,从而创建一个可从任务中抛出原始 AggregateException 的特殊可等待 模式成员。以下:

1     public static partial class TaskExtensions 2     { 3         [Description("Listing 15.03")] 4         public static AggregatedExceptionAwaitable WithAggregatedExceptions(this Task task) 5         { 6             if (task == null) 7             { 8                 throw new ArgumentNullException("task"); 9             }10 11             return new AggregatedExceptionAwaitable(task);12         }13 14         public struct AggregatedExceptionAwaitable15         {16             private readonly Task task;17 18             internal AggregatedExceptionAwaitable(Task task)19             {20                 this.task = task;21             }22 23             public AggregatedExceptionAwaiter GetAwaiter()24             {25                 return new AggregatedExceptionAwaiter(task);26             }27         }28 29         public struct AggregatedExceptionAwaiter : ICriticalNotifyCompletion30         {31             private readonly Task task;32 33             internal AggregatedExceptionAwaiter(Task task)34             {35                 this.task = task;36             }37 38             // Delegate most members to the task's awaiter39             public bool IsCompleted { get { return task.GetAwaiter().IsCompleted; } }40 41             public void UnsafeOnCompleted(Action continuation)42             {43                  task.GetAwaiter().UnsafeOnCompleted(continuation);44             }45 46             public void OnCompleted(Action continuation)47             {48                 task.GetAwaiter().OnCompleted(continuation);49             }50 51             public void GetResult()52             {53                 // This will throw AggregateException directly on failure,54                 // unlike task.GetAwaiter().GetResult()55                 task.Wait();56             }57         }58     }

2. 在抛出异常时进行包装

  你可能已经猜到我要说什么了,没错,就是异步方法在调用时永远不会直接抛出异常。异常方 法会返回 Task 或 Task<T> ,方法内抛出的任何异常(包括从其他同步或异步操作中传播过来的异 常)都将简单地传递给任务,就像前面介绍的那样。如果调用者直接等待任务,则可得到一个包 含真正异常的 AggregateException ;但如果调用者使用 await ,异常则会从任务中解包。返回 void 的异步方法可向原始的 SynchronizationContext 报告异常,如何处理将取决于上下文 。   

  除非你真的在乎为特定的上下文包装和解包异常,否则只需捕获嵌套的异步方法所抛出的异常即可。

1         private async static void MainSync() 2         { 3             Task
task = ReadFileAsync("fileName"); 4 try 5 { 6 string text = await task; 7 Console.WriteLine("file content {0}", text); 8 9 }10 catch (System.IO.IOException ex)11 {12 Console.WriteLine("caught exception {0}", ex.Message);13 }14 }15 private async static Task
ReadFileAsync(string fileName)16 {17 using (var reader = System.IO.File.OpenText(fileName))18 {19 return await reader.ReadToEndAsync();20 }21 }

  调用 File.OpenText 时可抛出一个 IOException (除非创建了一个名为“garbage file” 的文件),但如果 ReadToEndAsync 返回的任务失败了,也会出现同样的执行路径。在 MainAsync 中, ReadFileAsync 的调用 发生在进入 try 块之前,但只有在等待任务时 ,调用者才能看到 异常并在 catch 块中捕获 ,就像前面的 WebException 示例一样。同样,除异常发生的时机以 外,其行为我们也非常熟悉。

  与迭代器块类似,参数验证会有些麻烦。假设我们在验证完参数不含有空值后,想在异步方 法里做一些处理。如果像在同步代码中那样验证参数,那么在等待任务之前,调用者不会得到任 何错误提示。代码清单15-5给出了一个这样的例子。

1         static void Main(string[] args) 2         { 3             MainAsync().Wait(); 4  5             Console.ReadKey(); 6         } 7         private async static Task MainAsync() 8         { 9             Task
task = ComputeLengthAsync(null);10 Console.WriteLine("fetch the task");11 int length = await task;12 Console.WriteLine("length {0}", length);13 }14 private async static Task
ComputeLengthAsync(string text)15 {16 if (text == null)17 {18 throw new ArgumentNullException("text");19 }20 await Task.Delay(500);21 return text.Length;22 }

  代码清单15-5在失败前会先输出 Fetched the task 。实际上,在输出这条结果之前,异常 就已经同步地抛出了,这是因为在验证语句之前并不存在 await 表达式 。但调用代码直到等待 返回的任务时 ,才能看到这个异常。一般来说,参数验证无须耗时太久(或导致其他异步操作)。 最好能在系统陷入更大的麻烦以前,立即报告失败的存在。例如,如果对 HttpClient. GetStringAsync 传递一个空引用,则其可立即抛出异常。   

在C# 5中,有两种方式可以迫使异常立即抛出。第一种方式是将参数验证和实现分离,这与 代码清单6-9中处理迭代器块的情形相同。以下代码清单展示了 ComputeLengthAsync 的固定 版本。

代码清单15-6 将参数验证从异步实现中分离出来

1         private static Task
ComputeLengthAsync(string text) 2 { 3 if (text == null) 4 { 5 throw new ArgumentNullException("text"); 6 } 7 return ComputeLengthAsyncImpl(text); 8 } 9 private async static Task
ComputeLengthAsyncImpl(string text)10 {11 await Task.Delay(500); //模拟真正的异步工作12 return text.Length;13 }

  在代码清单15-6中,就语言形式而言, ComputeLengthAsync 本身并不是一个异步方法,因 为它没有 async 修饰符。该方法执行时使用的是正常的执行流,因此如果方法开始处的参数验证 抛出异常,就真的会抛出异常。而如果通过验证,则返回 ComputeLengthAsyncImpl 方法(工 作真正发生的地方)创建的任务。在更现实的场景中, ComputeLengthAsync 可以为公共或内 部(internal)方法,而 ComputeLengthAsyncImpl 应该为私有方法,因为它假设参数验证已经 执行过了。   

  另一个及早(eager)验证的方法是使用异步匿名函数,15.4节再来讨论这个示例。   

  异步方法中还有一种异常,其处理方式与其他异常不同,这个异常就是:取消(cancellation)。

3. 处理取消

  任务并行库(TPL)利用 CancellationTokenSource 和 CancellationToken 两种类型 向.NET 4中引入了一套统一的取消模型。该模型的理念是,创建一个 CancellationToken Source ,然后向其请求一个 CancellationToken ,并传递给异步操作。可在source上只执行取 消操作,但该操作会反映到token上。(这意味着你可以向多个操作传递相同的token,而不用担心 它们之间会相互干扰。)取消token有很多种方式,最常用的是调用 ThrowIfCancellation Requested ,如果取消了token,并且没有其他操作,则会抛出 OperationCanceledException 。 如果在同步调用(如 Task.Wait )中执行了取消操作,则可抛出同样的异常。

  C# 5规范中并没有说明取消操作如何与异步方法交互。根据规范,如果异步方法体抛出任何 异常,该方法返回的任务则将处于错误状态。“错误”的确切含义因实现而异,但实际上, 如 果 异 步 方 法 抛 出 OperationCanceledException ( 或 其 派 生 类 , 如 TaskCanceled Exception ),则返回的任务最终状态为 Canceled 。以下代码清单证实了导致任务取消的原因 确实是一个异常。

1         static void Main(string[] args) 2         { 3             Task task = ThrowCancellationException(); 4             Console.WriteLine(task.Status); 5             Console.ReadKey(); 6         } 7         private async static Task ThrowCancellationException() 8         { 9             throw new OperationCanceledException();10         }

这段代码的输出为 Canceld ,而不是 Faulted 。如果在任务上执行 Wait() ,或请求其结果 (针对 Task<T> ),则 AggregateException 内还是会抛出异常,所以没有必要在每次使用任务 时都显式检查是否有取消操作。

  重要的是,等待一个取消了的操作,将抛出原始的 OperationCanceledException 。这意 味着如果不采取一些直接的行动,从异步方法返回的任务同样会被取消,因为取消操作具有可传 播性。 代码清单15-8给出了一个有关任务取消操作的更为实际的例子。

1         static void Main(string[] args) 2         { 3             var source = new CancellationTokenSource(); 4             var task = DelayFor30Seconds(source.Token); 5             source.CancelAfter(TimeSpan.FromSeconds(1)); 6             Console.WriteLine("initial status {0}", task.Status); 7             try 8             { 9                 task.Wait();10             }11             catch (AggregateException ex)12             {13                 Console.WriteLine("caught {0}", ex.InnerExceptions[0]);14             }15             Console.WriteLine("final status {0}", task.Status);16             Console.ReadKey();17             /*  waiting for 30 seconds...18             initial status WaitingForActivation19             caught System.Threading.Tasks.TaskCanceledException: 已取消一个任务。20             final status Canceled   */21         }22         static async Task DelayFor30Seconds(CancellationToken token)23         {24             Console.WriteLine("waiting for 30 seconds...");25             await Task.Delay(TimeSpan.FromSeconds(30), token);26         }

  上述代码中启动了一个异步操作 ,该操作调用 Task.Delay 模拟真正的工作 ,并提供了 一个 CancellationToken 。这一次,我们的确涉及了多个线程:到达 await 表达式时,控制返 回到调用方法,这时要求 CancellationToken 在1秒后取消 。然后(同步地)等待任务完成 , 并期望在最终得到一个异常。最后展示任务的状态 。

  可认为取消操作默认是可传递的:如果A操作等待B操作,而B操作被取消了,那么我们认为 A操作也被取消了。 当然,你不必这么做。你可以在 DelayFor30Seconds 方法中捕获 OperationCanceled Exception ,然后或继续做其他事情,或立即返回,或干脆抛出一个其他类型的异常。异步特性 不会移除控制,它只是提供了一种有用的默认行为而已。

  【说明】 小心使用该代码 代码清单15-8在控制台程序中或从线程池线程调用时,均可运作良好。 但如果在Windows Forms UI线程(或其他单线程同步上下文)上执行这段代码,则会造成 死锁。能看出原因吗?想想在延迟任务完成时, DelayFor30Seconds 方法会试图返回到 哪个线程上?再想想 task.Wait() 调用运行在哪个线程上?这是个相对简单的例子,但 一些程序员在初次接触异步代码时往往会犯同样的错误。从根本上来说,问题在于调用 了 Wait() 方法或 Result 属性。在相关任务完成前,二者均可阻塞线程。我并不是说不能 使用它们,但在每次使用时必须考虑清楚。我们应该总是使用 await ,来异步地等待任务 的结果。

 

转载于:https://www.cnblogs.com/kikyoqiang/p/10124838.html

你可能感兴趣的文章
初入RFID技术
查看>>
电暖器选购指南(包括暖风机)
查看>>
各类常犯的错误总结
查看>>
mac打包python3程序
查看>>
Manacher's algorithm: 最长回文子串算法
查看>>
算法题003 斐波那契(Fibonacci)数列
查看>>
MySQL索引背后的数据结构及算法原理
查看>>
CSS定位 position
查看>>
冒泡排序
查看>>
es7新特性 includes用法
查看>>
block,inline和inline-block
查看>>
SQA
查看>>
Spring+Struts集成(方案一)
查看>>
在Windows 7中安装、配置和使用IIS7和ASP
查看>>
商业信息敏感、安全处理(口令、数字证书-U盾-密保卡、指纹识别-虹膜识别)...
查看>>
数据库设计的三大范式通俗解释
查看>>
H3C 典型数据链路层标准
查看>>
反向数据库表
查看>>
【原创】Elasticsearch无宕机迁移节点
查看>>
Stripe
查看>>