1. 在等待时拆包异常

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

         async Task<string> FetchFirstSuccessfulAsync(IEnumerable<string> urls)
{
//验证url
foreach (string url in urls)
{
try
{
using (var client=new HttpClient())
{
return await client.GetStringAsync(url);
}
}
catch (System.Net.WebException)
{
//记录日志
}
}
throw new System.Net.WebException("No urls Success");
}

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

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

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

     public static partial class TaskExtensions
{
[Description("Listing 15.03")]
public static AggregatedExceptionAwaitable WithAggregatedExceptions(this Task task)
{
if (task == null)
{
throw new ArgumentNullException("task");
} return new AggregatedExceptionAwaitable(task);
} public struct AggregatedExceptionAwaitable
{
private readonly Task task; internal AggregatedExceptionAwaitable(Task task)
{
this.task = task;
} public AggregatedExceptionAwaiter GetAwaiter()
{
return new AggregatedExceptionAwaiter(task);
}
} public struct AggregatedExceptionAwaiter : ICriticalNotifyCompletion
{
private readonly Task task; internal AggregatedExceptionAwaiter(Task task)
{
this.task = task;
} // Delegate most members to the task's awaiter
public bool IsCompleted { get { return task.GetAwaiter().IsCompleted; } } public void UnsafeOnCompleted(Action continuation)
{
task.GetAwaiter().UnsafeOnCompleted(continuation);
} public void OnCompleted(Action continuation)
{
task.GetAwaiter().OnCompleted(continuation);
} public void GetResult()
{
// This will throw AggregateException directly on failure,
// unlike task.GetAwaiter().GetResult()
task.Wait();
}
}
}

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

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

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

         private async static void MainSync()
{
Task<string> task = ReadFileAsync("fileName");
try
{
string text = await task;
Console.WriteLine("file content {0}", text); }
catch (System.IO.IOException ex)
{
Console.WriteLine("caught exception {0}", ex.Message);
}
}
private async static Task<string> ReadFileAsync(string fileName)
{
using (var reader = System.IO.File.OpenText(fileName))
{
return await reader.ReadToEndAsync();
}
}

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

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

         static void Main(string[] args)
{
MainAsync().Wait(); Console.ReadKey();
}
private async static Task MainAsync()
{
Task<int> task = ComputeLengthAsync(null);
Console.WriteLine("fetch the task");
int length = await task;
Console.WriteLine("length {0}", length);
}
private async static Task<int> ComputeLengthAsync(string text)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
await Task.Delay();
return text.Length;
}

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

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

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

         private static Task<int> ComputeLengthAsync(string text)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
return ComputeLengthAsyncImpl(text);
}
private async static Task<int> ComputeLengthAsyncImpl(string text)
{
await Task.Delay(); //模拟真正的异步工作
return text.Length;
}

  在代码清单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 。以下代码清单证实了导致任务取消的原因 确实是一个异常。

         static void Main(string[] args)
{
Task task = ThrowCancellationException();
Console.WriteLine(task.Status);
Console.ReadKey();
}
private async static Task ThrowCancellationException()
{
throw new OperationCanceledException();
}

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

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

         static void Main(string[] args)
{
var source = new CancellationTokenSource();
var task = DelayFor30Seconds(source.Token);
source.CancelAfter(TimeSpan.FromSeconds());
Console.WriteLine("initial status {0}", task.Status);
try
{
task.Wait();
}
catch (AggregateException ex)
{
Console.WriteLine("caught {0}", ex.InnerExceptions[]);
}
Console.WriteLine("final status {0}", task.Status);
Console.ReadKey();
/* waiting for 30 seconds...
initial status WaitingForActivation
caught System.Threading.Tasks.TaskCanceledException: 已取消一个任务。
final status Canceled */
}
static async Task DelayFor30Seconds(CancellationToken token)
{
Console.WriteLine("waiting for 30 seconds...");
await Task.Delay(TimeSpan.FromSeconds(), token);
}

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

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

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

最新文章

  1. 踏上Salesforce的学习之路(二)
  2. MVC 缓存实践(一)
  3. 为Office365增加密码过期自动提醒功能
  4. [SVN(ubuntu)] ubuntu使用svn
  5. Linux之cuda安装
  6. 6、四大组件之二-Service初步
  7. 最小生成树之Prim算法,Kruskal算法
  8. SQL Server 数据岸问题
  9. MVC设计模式JavaWeb实现
  10. [Swust OJ 1084]--Mzx0821月赛系列之情书(双线程dp)
  11. ubuntu虚拟机和主机互ping及secureCRT使用
  12. 关于vs2010下水晶报表的使用入门
  13. Java第8次实验(IO流)
  14. 第十一节、Harris角点检测原理(附源码)
  15. docker 应用-2(Dockerfile 编写以及镜像保存提交)
  16. poj3667 区间合并,找最左边的空余块
  17. 部署openresty遇到的一些坑
  18. JS 实现分页打印
  19. ECNU 3247 - 铁路修复计划
  20. 《C》VS控制台应用

热门文章

  1. tomcat理解
  2. pytest 失败用例重试
  3. cacti安装和第三方模块的导入
  4. @Validated数据校验
  5. 通过命令行升级git for windows
  6. 将TensorFlow模型变为pb——官方本身提供API,直接调用即可
  7. .NET通用基本权限系统框架源代码
  8. UESTC--1251--谕神的密码(贪心)
  9. bzoj 1050 [ HAOI 2006 ] 旅行comf —— 并查集
  10. DCloud-MUI:下拉刷新、上拉加载