使用async/await并行处理多个长时间运行的任务

本文关键字:长时间 运行 任务 async await 并行处理 使用 | 更新日期: 2023-09-27 18:28:28

我有一个助手方法返回IEnumerable<string>。随着收藏的增长,它的速度急剧放缓。我目前的方法主要是做以下几点:

var results = new List<string>();
foreach (var item in items)
{
    results.Add(await item.Fetch());
}

实际上,我不确定这种异步性是否给我带来了任何好处(它确实不像那样),但堆栈中的所有方法和我的控制器的操作都是异步的:

public async Task<IHttpActionResult> FetchAllItems()

由于这段代码最终由我的API使用,我真的很想将这些代码并行化,以实现我所希望的巨大加速。我试过了。AsParallel:

var results = items
    .AsParallel()
    .Select(i => i.Fetch().Result)
    .AsList();
return results;

And.WWhenAll(返回字符串[]):

var tasks = items.Select(i => i.Fetch());
return Task<string>.WhenAll<string>(tasks).Result;

最后一搏,解雇所有长期运行的工作,并依次等待它们(希望它们都是并行运行的,所以等待一个会让所有其他工作几乎完成):

var tasks = new LinkedList<Task<string>>();
foreach (var item in items)
    tasks.AddLast(item.Fetch());
var results = new LinkedList<string>();
foreach (var task in tasks)
    results.AddLast(task.Result);

在每个测试用例中,运行所需的时间与项目数量成正比。这样做没有明显的加速作用。使用Tasks和await/async缺少什么?

使用async/await并行处理多个长时间运行的任务

并行并发之间存在差异。并发只是意味着一次做多件事,而并行意味着在多个线程上做多件async非常适合并发性,但对并行性没有(直接)帮助。

一般来说,应该避免ASP.NET上的并行性。这是因为您所做的任何并行工作(即AsParallelParallel.ForEach等)都与ASP.NET共享相同的线程池,从而降低了ASP.NET处理其他请求的能力。这会影响web服务的可扩展性。最好将线程池留给ASP.NET。

然而,并发性很好,特别是异步并发性。这就是Task.WhenAll的用武之地。像这样的代码是你应该寻找的(注意,没有对Task<T>.Result的调用):

var tasks = items.Select(i => i.Fetch());
return await Task<string>.WhenAll<string>(tasks);

考虑到其他代码示例,最好从Fetch开始运行调用树,并用await替换所有Result调用。这可能是问题的一部分,因为Result强制执行同步。

另一个可能的问题是,正在获取的基础资源不支持并发访问,或者可能存在您不知道的节流。例如,如果Fetch从另一个web服务检索数据,请检查System.Net.ServicePointManager.DefaultConnectionLimit

与单个服务器的最大连接数也有一个可配置的限制,可以使下载性能与客户端线程数无关。

更改连接限制使用ServicePointManager.DefaultConnectionLimit

WebClient、HttpWebRequest和HttpClient 的最大并发请求数