TPL实现Task.WhileAll扩展方法

文章翻译整理自 Nikola Malovic 两篇博文:

当 Task.WhenAll 遇见 Task.WhenAny

在 TPL (Task Parallel Library) 中,有两种通过非阻塞方式等待 Task 数组任务结束的方式:Task.WhenAll 和 Task.WhenAny 。

它们的工作方式是:

  • WhenAll 当每项任务都完成时为完成。
  • WhenAny 当任意项任务完成时为完成。

现在我们需要一项功能,完成 Task 数组中的所有任务,并且当有任务完成时汇报状态。

我们称这个扩展方法为:Task.WhileAll 。

扩展方法实现

 1     public static class TaskExtensions
2 {
3 public static async Task<IList<T>> WhileAll<T>(this IList<Task<T>> tasks, IProgress<T> progress)
4 {
5 var result = new List<T>(tasks.Count);
6 var done = new List<Task<T>>(tasks);
7
8 while (done.Count > 0)
9 {
10 await Task.WhenAny(tasks);
11
12 var spinning = new List<Task<T>>(done.Count - 1);
13 for (int i = 0; i < done.Count; i++)
14 {
15 if (done[i].IsCompleted)
16 {
17 result.Add(done[i].Result);
18 progress.Report(done[i].Result);
19 }
20 else
21 {
22 spinning.Add(done[i]);
23 }
24 }
25
26 done = spinning;
27 }
28
29 return result;
30 }
31 }

代码实现很简单:

  • 其是 IList<Task<T>> 的一个 async 扩展方法
  • 方法返回完整的 IList<T> 结果
  • 方法会接受一个 IProgress<T> 类型的参数,用于向订阅者发布 Task 完成信息
  • 在方法体内,我们使用一个循环来检测,直到所有 Task 完成
  • 通过使用 Task.WhenAny 来异步等待 Task 完成

单元测试

 1     [TestClass]
2 public class UnitTest1
3 {
4 [TestMethod]
5 public async Task TestTaskExtensionsWhileAll()
6 {
7 var task1 = Task.Run(() => 101);
8 var task2 = Task.Run(() => 102);
9 var tasks = new List<Task<int>>() { task1, task2 };
10
11 List<int> result = new List<int>();
12 var listener = new Progress<int>(
13 taskResult =>
14 {
15 result.Add(taskResult);
16 });
17
18 var actual = await tasks.WhileAll(listener);
19 Thread.Sleep(50); // wait a bit for progress reports to complete
20
21 Assert.AreEqual(2, result.Count);
22 Assert.IsTrue(result.Contains(101));
23 Assert.IsTrue(result.Contains(102));
24
25 Assert.AreEqual(2, actual.Count);
26 Assert.IsTrue(actual.Contains(101));
27 Assert.IsTrue(actual.Contains(102));
28 }
29 }

同样,测试代码也不复杂:

  • 创建两个哑元 Task,并存到数组中
  • 定义进度侦听器 Progress<T>,来监测每个任务运行的结果
  • 通过 await 方式来调用方法
  • 使用 Thread.Sleep 来等待 50ms ,以便 Progress 可以来得及处理结果
  • 检查所有 Task 执行完毕后均已上报 Progress
  • 检查所有 Task 均已执行完毕

我知道每当使用 Thread.Sleep 时绝不是件好事,所以我决定摆脱它。

实现IProgressAsync<T>

问题实际上是因为 IProgress<T> 接口定义的是 void 委托,因此无法使用 await 进行等待。

因此我决定定义一个新的接口,使用同样的 Report 行为,但会返回 Task ,用以实现真正的异步。

1     public interface IProgressAsync<in T>
2 {
3 Task ReportAsync(T value);
4 }

有了异步版本的支持,将使订阅者更容易处理 await 调用。当然也可以使用 async void 来达成,但我认为 async void 总会延伸出更差的设计。所以,我还是选择通过定义 Task 返回值签名的接口来达成这一功能。

如下为接口实现:

 1     public class ProgressAsync<T> : IProgressAsync<T>
2 {
3 private readonly Func<T, Task> handler;
4
5 public ProgressAsync(Func<T, Task> handler)
6 {
7 this.handler = handler;
8 }
9
10 public async Task ReportAsync(T value)
11 {
12 await this.handler.InvokeAsync(value);
13 }
14 }

显然也没什么特别的:

  • 使用 Func<T, Task> 来代替 Action<T>,以便可以使用 await
  • ReportAsync 通过使用 await 方式来提供 Task

有了这些之后,我们来更新扩展方法:

 1     public static class TaskExtensions
2 {
3 public static async Task<IList<T>> WhileAll<T>(this IList<Task<T>> tasks, IProgressAsync<T> progress)
4 {
5 var result = new List<T>(tasks.Count);
6 var remainingTasks = new List<Task<T>>(tasks);
7
8 while (remainingTasks.Count > 0)
9 {
10 await Task.WhenAny(tasks);
11 var stillRemainingTasks = new List<Task<T>>(remainingTasks.Count - 1);
12 for (int i = 0; i < remainingTasks.Count; i++)
13 {
14 if (remainingTasks[i].IsCompleted)
15 {
16 result.Add(remainingTasks[i].Result);
17 await progress.ReportAsync(remainingTasks[i].Result);
18 }
19 else
20 {
21 stillRemainingTasks.Add(remainingTasks[i]);
22 }
23 }
24
25 remainingTasks = stillRemainingTasks;
26 }
27
28 return result;
29 }
30
31 public static Task InvokeAsync<T>(this Func<T, Task> task, T value)
32 {
33 return Task<Task>.Factory.FromAsync(task.BeginInvoke, task.EndInvoke, value, null);
34 }
35 }

所有都就绪后,我们就可以将 Thread.Sleep 从单元测试中移除了。

 1     [TestClass]
2 public class UnitTest1
3 {
4 private List<int> result = new List<int>();
5 private async Task OnProgressAsync(int arg)
6 {
7 result.Add(arg);
8 }
9
10 [TestMethod]
11 public async Task TestTaskExtensionsWhileAll()
12 {
13 var task1 = Task.Run(() => 101);
14 var task2 = Task.Run(() => 102);
15 var tasks = new List<Task<int>>() { task1, task2 };
16
17 var listener = new ProgressAsync<int>(this.OnProgressAsync);
18 var actual = await tasks.WhileAll(listener);
19
20 Assert.AreEqual(2, this.result.Count);
21 Assert.IsTrue(this.result.Contains(101));
22 Assert.IsTrue(this.result.Contains(102));
23
24 Assert.AreEqual(2, actual.Count);
25 Assert.IsTrue(actual.Contains(101));
26 Assert.IsTrue(actual.Contains(102));
27 }
28 }

Task.WhileAll扩展方法的更多相关文章

  1. TPL实现Task.WhileAll扩展方法

    文章翻译整理自 Nikola Malovic 两篇博文: Task.WhileAll Awaitable task progress reporting 当 Task.WhenAll 遇见 Task. ...

  2. Task扩展方法取消操作

    /// <summary> /// 任务扩展,传入取消操作 /// </summary> public static class TaskExtensionDemo { //因 ...

  3. .NET Core中间件的注册和管道的构建(2)---- 用UseMiddleware扩展方法注册中间件类

    .NET Core中间件的注册和管道的构建(2)---- 用UseMiddleware扩展方法注册中间件类 0x00 为什么要引入扩展方法 有的中间件功能比较简单,有的则比较复杂,并且依赖其它组件.除 ...

  4. 【转载】.NET(C#): Task.Unwrap扩展方法和async Lambda

    .NET(C#): Task.Unwrap扩展方法和async Lambda 目录 Task.Unwrap基本使用 Task.Factory.StartNew和Task.Run的Unwrap操作 使用 ...

  5. 用UseMiddleware扩展方法注册中间件类

    用UseMiddleware扩展方法注册中间件类 .NET Core中间件的注册和管道的构建(2)---- 用UseMiddleware扩展方法注册中间件类 0x00 为什么要引入扩展方法 有的中间件 ...

  6. 【ABP杂烩】Extensions后缀扩展方法

    1.Extensions介绍 扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型.重新编译或以其他方式修改原始类型. 扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用 ...

  7. ABP框架源码中的Linq扩展方法

    文件目录:aspnetboilerplate-dev\aspnetboilerplate-dev\src\Abp\Collections\Extensions\EnumerableExtensions ...

  8. ASP.NET Core中,UseDeveloperExceptionPage扩展方法会吃掉异常

    在ASP.NET Core中Startup类的Configure方法中,有一个扩展方法叫UseDeveloperExceptionPage,如下所示: // This method gets call ...

  9. 17-EasyNetQ:非泛型的发布&订阅扩展方法

    自从EasyNetQ第一个版本开始,它就可以发布/订阅特定类型的消息. bus.Subscribe<MyMessage>("subscriptionId", x =&g ...

随机推荐

  1. Eclipse UML小工具AmaterasUML的配置和使用

    AmaterasUML是个人认为最好用的Eclipse UML插件,能够通过拖拽Java源文件,轻松生成类图结构.同一时候支持活动图.时序图和用例图. 它的官方下载地址是:http://sourcef ...

  2. hdu 3460 Ancient Printer

    Problem Description The contest is beginning! While preparing the contest, iSea wanted to print the ...

  3. VC编程 快捷键增加的几种方式

    VB运行时菜单字母的下划线消失 vc 给菜单增加快捷键RT给Menu里面的 文件 帮助 查看 等功能键加上一个快捷方式.比如按Ctrl+F1 就弹出查看下面的子功能.------解决方案------- ...

  4. VC各种方法获得的窗口句柄

    AfxGetMainWnd AfxGetMainWnd获取窗口句柄本身 HWND hWnd = AfxGetMainWnd()->m_hWnd; GetTopWindow 功能:子窗体z序(Z序 ...

  5. [VirtualBox] - Install Oracle Linux 7 on Oracle VirtualBox

    I'll start coding with JEE soon. Product environment adopts Oracle + WebLogic in Linux, technology a ...

  6. POJ 3176-Cow Bowling(DP||记忆化搜索)

    Cow Bowling Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 14210   Accepted: 9432 Desc ...

  7. C--运算符,表达式和语句实例

    //第五章 运算符,表达式和语句 #include<stdio.h> //引入头文件 #include<math.h> #define ADJUST 7.64 //定义常量 # ...

  8. Linux MySQL自己环境搭建的笔记

    cd /usr/share/selinuxsetenforce 0tar -xvf MySQL-5.6.12-1.el6.x86_64.rpm-bundle.tarrpm -qa|grep -i my ...

  9. UIButton UIImage 用法分析

    一.UIButton和UIImageView的区别 1.显示图片 1> UIImageView只能显示一种图片(图片默认会填充整个UIImageView) image\setImage: 2&g ...

  10. 第4章1节《MonkeyRunner源码剖析》ADB协议及服务: ADB协议概览OVERVIEW.TXT翻译参考(原创)

    天地会珠海分舵注:本来这一系列是准备出一本书的,详情请见早前博文“寻求合作伙伴编写<深入理解 MonkeyRunner>书籍“.但因为诸多原因,没有如愿.所以这里把草稿分享出来,所以错误在 ...