等待异步谓词列表,但在第一个false时退出
本文关键字:第一个 false 退出 异步 谓词 列表 等待 | 更新日期: 2023-09-27 18:10:31
想象下面的类:
public class Checker
{
public async Task<bool> Check() { ... }
}
现在,想象一个该类的实例列表:
IEnumerable<Checker> checkers = ...
现在我想控制每个实例将返回true
:
checkers.All(c => c.Check());
现在,这将无法编译,因为Check()
返回Task<bool>
而不是bool
。
所以我的问题是:我怎样才能最好地枚举检查器列表?我怎么能在检查器返回false
时快捷枚举?(我认为All( )
已经做到了)
"异步序列"总是会引起一些混淆。例如,不清楚您想要的语义是否为:
- 同时启动所有检查,并在完成后对其进行评估。
- 每次启动一个检查,按顺序评估它们。
还有第三种可能(同时开始所有检查,并按顺序对它们进行评估),但在这种情况下,这将是愚蠢的。
我建议对异步序列使用Rx。它给了你很多选择,有点难学,但它也迫使你思考你到底想要什么。
以下代码将同时启动所有检查,并在检查完成时对其进行评估:
IObservable<bool> result = checkers.ToObservable()
.SelectMany(c => c.Check()).All(b => b);
它首先将检查器序列转换为一个可观察对象,对它们全部调用Check
,并检查它们是否都是true
。第一个以false
值结束的Check
将导致result
产生false
值。
相反,下面的代码将一次启动一个检查,按顺序计算它们:
IObservable<bool> result = checkers.Select(c => c.Check().ToObservable())
.Concat().All(b => b);
它首先将检查器序列转换为可观察对象序列,然后将这些序列连接起来(每次启动一个)。
如果你不想过多地使用可观察对象,也不想扰乱订阅,你可以直接await
它们。例如,在所有检查器上调用Check
并在它们完成时评估结果:
bool all = await checkers.ToObservable().SelectMany(c => c.Check()).All(b => b);
我如何在检查器返回false时快捷枚举?
这将按完成顺序检查任务的结果。因此,如果任务#5是第一个完成的任务,并返回false,则该方法立即返回false,而不考虑其他任务。较慢的任务(#1,#2等)永远不会被检查。
public static async Task<bool> AllAsync(this IEnumerable<Task<bool>> source)
{
var tasks = source.ToList();
while(tasks.Count != 0)
{
var finishedTask = await Task.WhenAny(tasks);
if(! finishedTask.Result)
return false;
tasks.Remove(finishedTask);
}
return true;
}
用法:
bool result = await checkers.Select(c => c.Check())
.AllAsync();
All
并没有考虑到async
(像所有LINQ
一样),所以你需要自己实现:
async Task<bool> CheckAll()
{
foreach(var checker in checkers)
{
if (!await checker.Check())
{
return false;
}
}
return true;
}
你可以用一个通用的扩展方法使它更易于重用:
public static async Task<bool> AllAsync<TSource>(this IEnumerable<TSource> source, Func<TSource, Task<bool>> predicate)
{
foreach (var item in source)
{
if (!await predicate(item))
{
return false;
}
}
return true;
}
并像这样使用:
var result = await checkers.AllAsync(c => c.Check());
你可以做
checkers.All(c => c.Check().Result);
,但这会同步运行任务,这可能非常慢,取决于Check()
的实现。
下面是一个功能齐全的测试程序,遵循dcastro的步骤:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AsyncCheckerTest
{
public class Checker
{
public int Seconds { get; private set; }
public Checker(int seconds)
{
Seconds = seconds;
}
public async Task<bool> CheckAsync()
{
await Task.Delay(Seconds * 1000);
return Seconds != 3;
}
}
class Program
{
static void Main(string[] args)
{
var task = RunAsync();
task.Wait();
Console.WriteLine("Overall result: " + task.Result);
Console.ReadLine();
}
public static async Task<bool> RunAsync()
{
var checkers = new List<Checker>();
checkers
.AddRange(Enumerable.Range(1, 5)
.Select(i => new Checker(i)));
return await checkers
.Select(c => c.CheckAsync())
.AllAsync();
}
}
public static class ExtensionMethods
{
public static async Task<bool> AllAsync(this IEnumerable<Task<bool>> source)
{
var tasks = source.ToList();
while (tasks.Count != 0)
{
Task<bool> finishedTask = await Task.WhenAny(tasks);
bool checkResult = finishedTask.Result;
if (!checkResult)
{
Console.WriteLine("Completed at " + DateTimeOffset.Now + "...false");
return false;
}
Console.WriteLine("Working... " + DateTimeOffset.Now);
tasks.Remove(finishedTask);
}
return true;
}
}
}
下面是示例输出:
Working... 6/27/2014 1:47:35 AM -05:00
Working... 6/27/2014 1:47:36 AM -05:00
Completed at 6/27/2014 1:47:37 AM -05:00...false
Overall result: False
请注意,当达到退出条件时,整个eval结束,而不会等待其余部分完成。
作为一种更开箱即用的替代方案,这似乎可以并行运行任务,并在第一次失败后不久返回:
var allResult = checkers
.Select(c => Task.Factory.StartNew(() => c.Check().Result))
.AsParallel()
.All(t => t.Result);
我对TPL和PLINQ不太感兴趣,所以请随时告诉我这有什么问题