【问题标题】:What is the proper way to send several async API requests and process the responses in parallel?发送多个异步 API 请求并并行处理响应的正确方法是什么?
【发布时间】:2023-04-19 22:51:02
【问题描述】:

我有一个项目列表,对于每个项目我需要执行几个异步 API 请求并处理响应,我已经看到了几个实现,它们在执行时间方面都很相似,但我想知道区别他们之间。

方法 1

Parallel.ForEach(items, new ParallelOptions { MaxDegreeOfParallelism = 10 }, item =>
        {
            var response = item.propertyOne.GetAsync().GetAwaiter().GetResult();
            //process it
            var response = item.propertyTwo.GetAsync().GetAwaiter().GetResult();
            //process it
            var response = item.propertyThree.GetAsync().GetAwaiter().GetResult();
            //process it
        });

方法 2

Parallel.ForEach(items, new ParallelOptions { MaxDegreeOfParallelism = 10 }, item =>
        {
            Task.Run(async () =>
            {
                var response = await item.propertyOne.GetAsync();
            }).GetAwaiter().GetResult();
            //process it
            Task.Run(async () =>
            {
                var response = await item.propertyTwo.GetAsync();
            }).GetAwaiter().GetResult();
            //process it
            Task.Run(async () =>
            {
                var response = await item.propertyThreee.GetAsync();
            }).GetAwaiter().GetResult();
            //process it
        });

方法 3 假设应用了一些边界机制,以免系统充满任务

List<Task> tasksToAwait = new List<Task>();
        foreach (var item in items)
        {
            tasksToAwait.Add(Task.Run(async () =>
            {
                var response = await item.propertyOne.GetAsync();
                //process it
                var response = await item.propertyTwo.GetAsync();
                //process it
                var response = await item.propertyThree.GetAsync();
                //process it
            }));
        }
        await Task.WhenAll(taskToAwait);

注意事项:

  • 我使用 GetAwaiter().GetResult() 而不是 Wait(),因为它不会吞下异常。
  • 必须以某种方式等待请求才能处理响应。
  • 这是作为后台任务执行的,所以我不介意阻塞调用线程。
  • 第三种方法略快于其他两种方法,第二种方法略快于第一种方法。
  • 我无法控制通过 GetAsync() 调用的异步 API。

推荐其中哪一项,如果没有,您有什么建议?另外,它们有什么不同,为什么会有执行时间差异?

【问题讨论】:

  • 好问题,我通常只是将任务存储在列表中,然后执行WhenAll

标签: c# asp.net-core task parallel.foreach


【解决方案1】:

由于您的方法是异步的,只需调用它们,而不是单独等待它们,等待它们以确保所有三个异步过程都完成。然后,您可以再次使用await 来解包结果:

// start all asynchronous processes at once for all three properties
var oneTask = item.propertyOne.GetAsync();
var twoTask = item.propertyTwo.GetAsync();
var threeTask = item.propertyThree.GetAsync();

// await the completion of all of those tasks
await Task.WhenAll(oneTask, twoTask, threeTask);

// access the individual results; since the tasks are already completed, this will
// unwrap the results from the tasks instantly:
var oneResult = await oneTask;
var twoResult = await twoTask;
var threeResult = await threeTask;

一般来说,在处理异步的东西时,不惜一切代价避免调用.GetResult().Result.Wait()。如果你有一个异步进程,这些都不会有任何好处。


回复您的 cmets:

我假设代码会进入并行 foreach 对吗?

不,您不会在这里使用Parallel.ForEach,因为您仍在调用异步进程。 Parallel.ForEach 用于将 work 加载到多个线程,但这不是你想要做的。您拥有可以同时运行的异步进程,而无需额外的线程。

因此,如果您有很多这样的事情要异步调用,只需将它们全部调用并收集Tasks 以便稍后等待它们。

但是如果请求必须是连续的,例如分页响应,其中第一个请求会返回一个 URL 用于下一个页面请求等等?

然后,您需要使调用相互依赖,您使用await 确保在您可以继续之前完成异步过程。例如,如果propertyTwo 调用依赖于propertyOne,那么您仍然可以先进行所有 propertyOne 调用,并且仅在这些调用完成后继续。

如果您将逻辑拆分为单独的方法,您会发现这变得更容易:

var itemTasks = items.Select(item => GetPropertiesOneTwoThreeAsync(item));
await Task.WhenAll(itemTasks);

private Task GetPropertiesOneTwoThreeAsync(Item item)
{
    // get the properties sequentially
    var oneResult = await item.propertyOne.GetAsync();
    var twoResult = await item.propertyTwo.GetAsync();
    var threeResult = await item.propertyThree.GetAsync();
}

另外,如果调用方法不是异步的,Task.WhenAll(oneTask, twoTask, threeTask).GetAwaiter().GetResult() 在这种情况下是否合适?

没有。从同步方法调用异步方法通常不是您应该做的事情。如果调用异步方法,则应使调用方法也异步。有一种方法可以完成这项工作,但你必须处理潜在的死锁,并且通常应该知道你在做什么。另见this question

【讨论】:

  • 好主意,我假设代码会进入并行 foreach 对吗?但是,如果请求必须是顺序的,例如第一个请求将返回下一页请求的 URL 的分页响应等,该怎么办?另外,如果调用方法不是异步的,Task.WhenAll(oneTask, twoTask, threeTask).GetAwaiter().GetResult() 在这种情况下是否合适?谢谢
  • @DaniMazahreh 我编辑了我的答案以回答您的后续问题。
  • 欣赏后续,最后的sn-p代码类似于我将坚持的第三种方法。
【解决方案2】:

使用 Parallel.Foreach 将所有任务添加到 List 并使用 WhenAll 方法等待所有任务完成。

【讨论】:

  • 除了加快任务创建速度之外,还能实现什么?