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

当 Task.WhenAll 遇见 Task.WhenAny

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

它们的工作方式是:

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

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

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

扩展方法实现

     public static class TaskExtensions
{
public static async Task<IList<T>> WhileAll<T>(this IList<Task<T>> tasks, IProgress<T> progress)
{
var result = new List<T>(tasks.Count);
var done = new List<Task<T>>(tasks); while (done.Count > )
{
await Task.WhenAny(tasks); var spinning = new List<Task<T>>(done.Count - );
for (int i = ; i < done.Count; i++)
{
if (done[i].IsCompleted)
{
result.Add(done[i].Result);
progress.Report(done[i].Result);
}
else
{
spinning.Add(done[i]);
}
} done = spinning;
} return result;
}
}

代码实现很简单:

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

单元测试

     [TestClass]
public class UnitTest1
{
[TestMethod]
public async Task TestTaskExtensionsWhileAll()
{
var task1 = Task.Run(() => );
var task2 = Task.Run(() => );
var tasks = new List<Task<int>>() { task1, task2 }; List<int> result = new List<int>();
var listener = new Progress<int>(
taskResult =>
{
result.Add(taskResult);
}); var actual = await tasks.WhileAll(listener);
Thread.Sleep(); // wait a bit for progress reports to complete Assert.AreEqual(, result.Count);
Assert.IsTrue(result.Contains());
Assert.IsTrue(result.Contains()); Assert.AreEqual(, actual.Count);
Assert.IsTrue(actual.Contains());
Assert.IsTrue(actual.Contains());
}
}

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

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

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

实现IProgressAsync<T>

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

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

     public interface IProgressAsync<in T>
{
Task ReportAsync(T value);
}

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

如下为接口实现:

     public class ProgressAsync<T> : IProgressAsync<T>
{
private readonly Func<T, Task> handler; public ProgressAsync(Func<T, Task> handler)
{
this.handler = handler;
} public async Task ReportAsync(T value)
{
await this.handler.InvokeAsync(value);
}
}

显然也没什么特别的:

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

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

     public static class TaskExtensions
{
public static async Task<IList<T>> WhileAll<T>(this IList<Task<T>> tasks, IProgressAsync<T> progress)
{
var result = new List<T>(tasks.Count);
var remainingTasks = new List<Task<T>>(tasks); while (remainingTasks.Count > )
{
await Task.WhenAny(tasks);
var stillRemainingTasks = new List<Task<T>>(remainingTasks.Count - );
for (int i = ; i < remainingTasks.Count; i++)
{
if (remainingTasks[i].IsCompleted)
{
result.Add(remainingTasks[i].Result);
await progress.ReportAsync(remainingTasks[i].Result);
}
else
{
stillRemainingTasks.Add(remainingTasks[i]);
}
} remainingTasks = stillRemainingTasks;
} return result;
} public static Task InvokeAsync<T>(this Func<T, Task> task, T value)
{
return Task<Task>.Factory.FromAsync(task.BeginInvoke, task.EndInvoke, value, null);
}
}

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

     [TestClass]
public class UnitTest1
{
private List<int> result = new List<int>();
private async Task OnProgressAsync(int arg)
{
result.Add(arg);
} [TestMethod]
public async Task TestTaskExtensionsWhileAll()
{
var task1 = Task.Run(() => );
var task2 = Task.Run(() => );
var tasks = new List<Task<int>>() { task1, task2 }; var listener = new ProgressAsync<int>(this.OnProgressAsync);
var actual = await tasks.WhileAll(listener); Assert.AreEqual(, this.result.Count);
Assert.IsTrue(this.result.Contains());
Assert.IsTrue(this.result.Contains()); Assert.AreEqual(, actual.Count);
Assert.IsTrue(actual.Contains());
Assert.IsTrue(actual.Contains());
}
}

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

  1. Task.WhileAll扩展方法

    TPL实现Task.WhileAll扩展方法 文章翻译整理自 Nikola Malovic 两篇博文: Task.WhileAll Awaitable task progress reporting ...

  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. Linux下的段错误(Segmentation fault)

    Linux开发中常见段错误问题原因分析 1 使用非法的内存地址(指针),包括使用未经初始化及已经释放的指针.不存在的地址.受系统保护的地址,只读的地址等,这一类也是最常见和最好解决的段错误问题,使用G ...

  2. DIRECTORY_SEPARATOR 和 PATH_SEPARATOR的区别

    DIRECTORY_SEPARATOR:目录分隔符,linux上就是’/’    windows上是’\’ PATH_SEPARATOR:路径分隔符,include多个路径使用,在win下,当你要in ...

  3. HTTP的应用httpclient 和线程

    package com.example.service; import java.io.IOException; import java.io.InputStream; import org.apac ...

  4. CentOS使用virtualenv搭建独立的Python环境-python虚拟环境

    CentOS使用virtualenv搭建独立的Python环境-python虚拟环境 virtualenv可以搭建虚拟且独立的python环境,可以使每个项目环境与其他项目独立开来,保持环境的干净,解 ...

  5. linux 下文件节点索引

    最近发现一个奇怪的问题,就是一个pyhton 后台的服务一直打印日志文件,在中间我用vim看日志文件,关闭时习惯性的:wq退出,在此之后日志文件就不输出了. 1 对于这个现象我开始认为是python ...

  6. 【DIOCP知识库】连接上下文TIocpClientContext

    来自弦子介绍 [概述] 该类管理远程连接,每一个远程连接会对应一个该类的实例,开发时可以通过继承该类,扩展属性,可以存储更多的连接信息,可以重写OnRecvBuffer方法进行数据逻辑的处理 [字段/ ...

  7. Ruby-打包程序

    1.下载 ocra-1.3.1.gem 文件 2.打开“start Command prompt with ruby”  命令窗口 3.输入 “gem install e:\ocra-1.3.1.ge ...

  8. [aspx]控件及代码小例

    1. 原生 asp 方式遍历 DataTable  2. aspx 的控件 Repeater 后台绑定 <%-- DataTable dt = OleDbHelper.GetTable(&quo ...

  9. 算法与数据结构实验题 5.2 Missile

    1.题目: 2.解题思路: 把每个点对应的两条半径求出,之后对d1进行升序排序,对应d2也改变位置.其中一个圆心的半径r1确定之后,除去第一个圆包围的点,在其余点中找到另外一个圆的最长的半径r2,此时 ...

  10. AngularJS学习--- AngularJS中XHR(AJAX)和依赖注入(DI) step5

    前言:本文接前一篇文章,主要介绍什么是XHR,AJAX,DI,angularjs中如何使用XHR和DI. 1.切换工具目录 git checkout -f step- #切换分支 npm start ...