对于C# 5异步特性,我最喜欢的一点是它可以自然而然地组合在一起。这表现为两种不同的 方式。最明显的是,异步方法返回任务,并通常会调用其他返回任务的方法。这些方法可以是直 接的异步操作(如链的最底部),也可以是更多的异步方法。所有的包装和拆包都需要将结果转 换为任务,反向操作则由编译器完成。

  另一种组合形式是,创建与操作无关的构建块来管理任务的处理。这些构建块无须知道任务 在做什么,而只是单纯待在 Task<T> 的抽象级别。这有点像LINQ操作符,只是面向的是任务而 不是序列。框架中内置了一些构建块,但也可以自行创建。

1. 在单个调用中收集结果

  例如,尝试获取若干URL。15.3.6节中一次性获取了所有URL,并在完成任务后立即停止获 取。假设这次要启动多个并行请求,然后每得到一个URL就记录下结果。记住,异步方法返回的 是已经运行的任务,因此可非常轻松地为每个URL启动一个任务:

             string[] urls = new string[] { "http://stackoverflow.com", "http://www.google.com", "http://csharpindepth.com" };
var tasks = urls.Select(async url =>
{
using (var client = new HttpClient())
{
return await client.GetStringAsync(url);
}
}).ToList();

  注意,需调用 ToList() 来具体化LINQ查询。这保证了每个任务将只启动一次。否则每次迭 代 tasks 时,将会再次获取字符串。(如不释放 HttpClient ,代码会更加简单,但即便如此,代 码也不是很难看。)

  TPL提供了一个 Task.WhenAll 方法,从而将各有一个结果的多个任务组合成一个包含多个 结果的任务。常用的方法重载签名如下所示:

        public static Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks);

  这个声明看上去非常糟糕,但在真正使用时,会发现其方法目的非常单纯。你将得到一个 List<Task<string>> ,因此可以写为:

            string[] results = await Task.WhenAll(tasks);

  所有任务均已结束,并将结果收集到一个数组中后,等待方可终止。本章前面讲过,如果多 个任务抛出异常,则只有第一个异常会立即抛出,但可总是迭代这些任务,以找到具体失败的任 务及其失败原因,或使用代码清单15-2中所示的 WithAggregatedException 扩展方法。

  如果只关注第一个返回的请求,则可使用 Task.WhenAny 方法。该方法不会等待第一个成功 完成的任务,而只会等待第一个到达终点状态的任务。

  本例中,你可能想要点特别的做法。在任务完成后报告全部结果可能会更有用些。

2. 在全部完成时收集结果

  Task.WhenAll 是.NET内置的转换构建块(transformational building block),接下来将介绍如何以类似的方式构建自己的方法。TAP文档中含有类似的示例代码,从而创建了 Interleaved 方法,这里将介绍另一版本。

  代码清单15-12旨在传递一个输入任务的序列,并返回一个输出任务的序列。两个序列中任 务的结果是相同的,但存在一个重要差异,即输出任务的完成顺序与输入完全一样,因此可以一 次 await 一个任务,并可立即得到任务结果。这听上去有些神奇,对我来说也是如此,因此我们 来看看代码,研究一下它的工作原理。

         public static IEnumerable<Task<T>> InCompletionOrder<T>(this IEnumerable<Task<T>> source)
{
var inputs = source.ToList();
var boxes = inputs.Select(x => new TaskCompletionSource<T>()).ToList(); int currentIndex = -;
foreach (var task in inputs)
{
task.ContinueWith(completed =>
{
var nextBox = boxes[Interlocked.Increment(ref currentIndex)];
PropagateResult(completed, nextBox);
}, TaskContinuationOptions.ExecuteSynchronously);
}
return boxes.Select(box => box.Task);
}

  代码清单15-12依赖TPL中一个非常重要的类型,即 TaskCompletionSource<T> 。该类型 可用于创建一个尚未含有结果的 Task ,并在之后提供结果(或异常)。它和 AsyncTaskMethod Builder<T> 都建立在相同的基础结构之上。后者为异步方法提供返回的 Task ,并在方法体完成 时,将带结果的任务向外传播。

  为什么会用这么奇怪的变量名( boxes )呢?我常常把任务想象成纸箱,这些纸箱承诺 (promise)在某个时刻,其内部会含有值或错误。 TaskCompletionSource<T> 就像是背面有洞 的箱子,你可以把它给别人,然后再偷偷地把值从洞口塞进去 ① 。这正是 PropagateResult 方法 的作用,不过它没那么有意思,所以此处不予列出,基本上它会将已完成的 Task<T> 的结果传播 到 TaskCompletionSource<T> 中。如果原始任务正常完成,则将返回值复制到 Task CompletionSource<T> 中。如果原始任务产生了错误,则可将异常复制到 TaskCompletion Source<T> 中。取消原始任务后, TaskCompletionSource<T> 也会随之被取消。

  真正聪明的部分是(我对此说法不承担任何责任——有人发邮件建议我加入这一免责声明), 在该方法运行时,它并不知道哪个 TaskCompletionSource<T> 会对应哪个输入任务,而只是将 相同的后续操作附加到各任务上,然后由后续操作来寻找下一个 TaskCompletionSource<T> (通过对一个计数器进行原子地累加)并传播结果。也就是说,它会按照原始任务的输出顺序对箱子进行填充。

  图15-5展示了三个输入任务,以及相应的由方法返回的输出任务。即使输入任务的顺序与方 法返回的顺序不同,输出任务的顺序也会与之相同。 有了这个绝妙的扩展方法后,即可编写代码清单15-13,从而得到一组URL,并行地对每个 URL发起请求,并在请求完成时写下各页面的长度,然后返回总长度。

         static void Main()
{
var task = ShowPageLengthsAsync("http://stackoverflow.com", "http://www.google.com", "http://csharpindepth.com");
Console.WriteLine("Total length: {0}", task.Result);
}
static async Task<int> ShowPageLengthsAsync(params string[] urls)
{
var tasks = urls.Select(async url =>
{
using (var client = new HttpClient())
{
return await client.GetStringAsync(url);
}
}).ToList(); int total = ;
foreach (var task in tasks.InCompletionOrder())
{
string page = await task;
Console.WriteLine("Got page length {0}", page.Length);
total += page.Length;
}
return total;
}

  代码清单15-13存在两个小问题。
  1.一个任务失败,则整个异步操作都将失败,并且不会保留结果。这也许没问题,但也可能希望能够将每次失败记录下来。(与.NET 4不同,不处理任务异常,则默认不会让进程当掉,但至少应考虑对其他任务产生的影响。)
    2.失去对页面转向具体URL的跟踪。
这两个问题都可以通过少量代码轻松解决,也可以进一步提取成可复用的构建块。举这些例子并不是为了满足个别需求,而是为了让你接受组合带来的各种可能性。TAP白皮书中并不是只有 Interleaved 这一个例子,它还包括很多概念,并附带一些有助于理解的示例。

最新文章

  1. Django项目流程(摘抄整理)
  2. Spark入门实战系列--5.Hive(上)--Hive介绍及部署
  3. iOS如何获取网络图片(二)
  4. Runner之记计帐项目的典型用户和用户场景
  5. Secure Socket Tunneling Protocol Service服务无法启动(win7)
  6. IAP沙盒测试帐号无法购买的问题
  7. 初识-----基于Socket的UDP和TCP编程及测试代码
  8. select 中使用 case when 和 replace
  9. Java深入解析读书笔记(一)
  10. Codeforces 450 C. Jzzhu and Chocolate
  11. egret dragonbones部件替换产生位移的解决方案
  12. (1)ES6中let,const,对象冻结,跨模块常量,新增的全局对象介绍
  13. [js高手之路]Node.js模板引擎教程-jade速学与实战4-模板引用,继承,插件使用
  14. Android 圆角的效果实现
  15. 移除Excel工作表密码保护小工具含C#源代码
  16. js中的arguments
  17. 原来bug解决了,是这样的感觉
  18. solidity ecrecover
  19. MySql服务的启动和停止
  20. ini_set的用法介绍

热门文章

  1. 表格属就用treegrid
  2. 可编程数据平面将OpenFlow扩展至电信级应用(一)
  3. word设置
  4. STM32跑马灯
  5. 更改App.config里的值并保存
  6. HDUOJ--2121--Ice_cream’s world II【朱刘算法】不定根最小树形图
  7. YTU 2760: 字符串---首字母变大写
  8. 如何通过XInput技术针对游戏方向盘或者手柄编程
  9. Flink之Window Operation
  10. Linux VPS上安装KDE, Gnome和VNC