我有一个像这样的异步谓词方法:
private async Task<bool> MeetsCriteria(Uri address)
{
//Do something involving awaiting an HTTP request.
}
假设我收集了Uri
s:
var addresses = new[]
{
new Uri("http://www.google.com/"),
new Uri("http://www.stackoverflow.com/") //etc.
};
我想使用addresses
过滤MeetsCriteria
。我想异步这样做;我希望多次调用谓词以异步方式运行,然后我想等待所有这些调用完成并生成过滤结果集。不幸的是,LINQ似乎不支持异步谓词,所以像这样的东西不能工作:
var filteredAddresses = addresses.Where(MeetsCriteria);
有同样方便的方法吗?
答案 0 :(得分:9)
我认为在框架中没有这样的原因之一是存在许多可能的变化,并且在某些情况下每个选择都是正确的:
Task<IEnumerable<T>>
更改为其他内容。)你说你希望谓词并行执行。在这种情况下,最简单的选择是一次性执行它们并按完成顺序返回它们:
static async Task<IEnumerable<T>> Where<T>(
this IEnumerable<T> source, Func<T, Task<bool>> predicate)
{
var results = new ConcurrentQueue<T>();
var tasks = source.Select(
async x =>
{
if (await predicate(x))
results.Enqueue(x);
});
await Task.WhenAll(tasks);
return results;
}
然后您可以像这样使用它:
var filteredAddresses = await addresses.Where(MeetsCriteria);
答案 1 :(得分:7)
第一种方法:一个接一个地预先发出所有请求,然后等待所有请求返回,然后过滤结果。 (svick的代码也做了这个,但是我在没有中间ConcurrentQueue的情况下这样做。)
// First approach: massive fan-out
var tasks = addresses.Select(async a => new { A = a, C = await MeetsCriteriaAsync(a) });
var addressesAndCriteria = await Task.WhenAll(tasks);
var filteredAddresses = addressAndCriteria.Where(ac => ac.C).Select(ac => ac.A);
第二种方法:一个接一个地执行请求。这将需要更长的时间,但它将确保不会因为大量的请求而破坏Web服务(假设MeetsCriteriaAsync发送到Web服务......)
// Second approach: one by one
var filteredAddresses = new List<Uri>();
foreach (var a in filteredAddresses)
{
if (await MeetsCriteriaAsync(a)) filteredAddresses.Add(a);
}
第三种方法:至于第二种方法,但使用假设的C#8特征&#34;异步流&#34;。 C#8还没有出现,异步流还没有设计,但我们可以做梦! IAsyncEnumerable类型已经存在于RX中,并且希望它们能够为它添加更多的组合器。关于IAsyncEnumerable的好处是,我们可以在它们到来时立即开始使用前几个过滤后的地址,而不是等待首先过滤所有内容。
// Third approach: ???
IEnumerable<Uri> addresses = {...};
IAsyncEnumerable<Uri> filteredAddresses = addresses.WhereAsync(MeetsCriteriaAsync);
第四种方法:也许我们不想一次性处理所有请求的网络服务,但我们很乐意一次发出多个请求。也许我们做了实验,发现&#34;一次三个&#34;是一个快乐的媒介。注意:此代码假定单线程执行上下文,例如UI编程或ASP.NET。如果它在多线程执行上下文中运行,那么它需要一个ConcurrentQueue和ConcurrentList。
// Fourth approach: throttle to three-at-a-time requests
var addresses = new Queue<Uri>(...);
var filteredAddresses = new List<Uri>();
var worker1 = FilterAsync(addresses, filteredAddresses);
var worker2 = FilterAsync(addresses, filteredAddresses);
var worker3 = FilterAsync(addresses, filteredAddresses);
await Task.WhenAll(worker1, worker2, worker3);
async Task FilterAsync(Queue<Uri> q, List<Uri> r)
{
while (q.Count > 0)
{
var item = q.Dequeue();
if (await MeetsCriteriaAsync(item)) r.Add(item);
}
}
使用TPL数据流库的第四种方法也有办法。
答案 2 :(得分:2)
我认为这比没有使用任何concurrentQueue的接受答案更简单。
public static async Task<IEnumerable<T>> Where<T>(this IEnumerable<T> source, Func<T, Task<bool>> predicate)
{
var results = await Task.WhenAll(source.Select(async x => (x, await predicate(x))));
return results.Where(x => x.Item2).Select(x => x.Item1);
}
答案 3 :(得分:0)
我将使用以下方法,而不是使用ConcurrentBag
或ConcurrentQueue
public static async IAsyncEnumerable<T> WhereAsync<T>(this IEnumerable<T> source, Func<T, Task<bool>> predicate)
{
foreach(var item in source)
{
if(await (predicate(item)))
{
yield return item;
}
}
}
例如
var result = numbers.WhereAsync(async x =>
await IsEvenAsync(x));
await foreach (var x in result)
{
Console.Write($"{x},");
}
答案 4 :(得分:0)
考虑到框架的较新版本和 IAsyncEnumerable<T>
接口的采用,我不会再在这里推荐任何其他高度自定义的答案,因为它们基本上是不必要的。
LINQ 的异步版本可通过 the System.Linq.Async
NuGet package 获得。
这是进行异步检查的语法:
var filteredAddresses = addresses
.ToAsyncEnumerable()
.WhereAwait(async x => await MeetsCriteria(x));
filteredAddresses
的类型为 IAsyncEnumerable<int>
,可以是:
ToListAsync
、FirstAsync
等实现await foreach
迭代要获得和之前一样的效果并允许使用方法组调用,您可以将MeetsCriteria
的返回类型更改为ValueTask
:
private async ValueTask<bool> MeetsCriteria(Uri address)
{
//Do something involving awaiting an HTTP request.
}
...
var filteredAddresses = addresses
.ToAsyncEnumerable()
.WhereAwait(MeetsCriteria);
不过,我不建议仅使用 ValueTask
来保存几个字符,因为应该对其进行基准测试并出于性能/内存原因使用它。