zoukankan      html  css  js  c++  java
  • 15.3 Task 异常

    1. 在等待时拆包异常

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

     1         async Task<string> FetchFirstSuccessfulAsync(IEnumerable<string> 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 AggregatedExceptionAwaitable
    15         {
    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 : ICriticalNotifyCompletion
    30         {
    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 awaiter
    39             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<string> 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<string> 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<int> 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<int> 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<int> 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<int> 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 WaitingForActivation
    19             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 ,来异步地等待任务 的结果。

  • 相关阅读:
    IOS断点下载
    IOS中手机、pad的坐标系
    修改UINavigationController返回按钮的标题及如何隐藏导航栏
    备份数据脚本
    判断apache是否启动的脚本
    MySQL utf8mb4 字符集:支持 emoji 表情符号
    opacity兼容写法
    返回顶部(解决IE6固定定位)
    CSS3字体图标
    css3旋转小三角
  • 原文地址:https://www.cnblogs.com/kikyoqiang/p/10124838.html
Copyright © 2011-2022 走看看