在for-each-loop中等待异步调用[重复]

Posted

技术标签:

【中文标题】在for-each-loop中等待异步调用[重复]【英文标题】:Await async call in for-each-loop [duplicate] 【发布时间】:2019-02-28 20:33:11 【问题描述】:

我有一个检索部署列表的方法。对于每个部署,我想检索一个关联的版本。因为所有调用都是对外部 API 进行的,所以我现在有一个 foreach 循环,可以在其中进行这些调用。

public static async Task<List<Deployment>> GetDeployments()

    try
    
        var depjson     = await GetJson($"BASEURLrelease/deployments?deploymentStatus=succeeded&definitionId=2&definitionEnvironmentId=5&minStartedTime=MinDateTime");
        var deployments = (JsonConvert.DeserializeObject<DeploymentWrapper>(depjson))?.Value?.OrderByDescending(x => x.DeployedOn)?.ToList();

        foreach (var deployment in deployments)
        
            var reljson = await GetJson($"BASEURLrelease/releases/deployment.ReleaseId");
            deployment.Release = JsonConvert.DeserializeObject<Release>(reljson);
        

        return deployments;
    
    catch (Exception)
    
        throw;
    

这一切都很好。但是,我根本不喜欢 foreach 循环中的 await。我也认为这不是好的做法。我只是不知道如何重构它,所以调用是并行的,因为每个调用的结果都用于设置部署的属性。

如果有任何关于如何使此方法更快的建议,我将不胜感激,并尽可能避免在 foreach 循环中使用 await-ing。

【问题讨论】:

你可以使用 Parallel.Foreach() 或使用 PLinq 或创建一个任务 foreach GetJson() 并等待他们都看到:Use Task.WaitAll() to handle awaited tasks? 或者创建一个List&lt;Task&lt;...&gt;&gt;,将每个调用添加到api然后await Task.WhenAll(tasks)gigi.nullneuron.net/gigilabs/avoid-await-in-foreach 这是TPL DataFlow的工作,是的 @J.vanLangen 谢谢,虽然我已经读到Parallel.ForEach 是推荐的 CPU 密集型任务,而不是与 IO 相关的任务。正如@Ric 提到的,我确实可以将每个对API 的调用放在List&lt;Task&lt;..&gt;&gt; 中,然后稍后按ID 映射正确的Release。我还将研究提到的 TPL DataFlow @Saruman。谢谢! 【参考方案1】:

如果我理解你的正确,并且你想制作var reljson = await GetJson parralel:

试试这个:

Parallel.ForEach(deployments, (deployment) =>

    var reljson = await GetJson($"BASEURLrelease/releases/deployment.ReleaseId");
    deployment.Release = JsonConvert.DeserializeObject<Release>(reljson);
);

您可能会限制并行执行的数量,例如:

Parallel.ForEach(
    deployments,
    new ParallelOptions  MaxDegreeOfParallelism = 4 ,
    (deployment) =>

    var reljson = await GetJson($"BASEURLrelease/releases/deployment.ReleaseId");
    deployment.Release = JsonConvert.DeserializeObject<Release>(reljson);
);

您可能还希望能够打破循环:

Parallel.ForEach(deployments, (deployment, state) =>

    var reljson = await GetJson($"BASEURLrelease/releases/deployment.ReleaseId");
    deployment.Release = JsonConvert.DeserializeObject<Release>(reljson);
    if (noFurtherProcessingRequired) state.Break();
);

【讨论】:

Parallel.For/ForeachIO 绑定 工作的错误工具,您锁定资源等待 IO 完成端口,TPL Dataflow 更适合此 感谢您的建议,尽管正如@Saruman 已经提到的,我也认为Parallel.ForEach 应该主要用于 CPU 密集型任务,而不是 IO 任务。 Parallel.ForEach 不支持等待。【参考方案2】:

你现在所做的并没有错。但是有一种方法可以一次调用所有任务,而不是等待单个任务,然后处理它,然后再等待另一个。

你可以这样转:

wait for one -&gt; process -&gt; wait for one -&gt; process ...

进入

wait for all -&gt; process -&gt; done

转换这个:

foreach (var deployment in deployments)

    var reljson = await GetJson($"BASEURLrelease/releases/deployment.ReleaseId");
    deployment.Release = JsonConvert.DeserializeObject<Release>(reljson);

收件人:

var deplTasks = deployments.Select(d => GetJson($"BASEURLrelease/releases/d.ReleaseId"));
var reljsons = await Task.WhenAll(deplTasks);
for(var index = 0; index < deployments.Count; index++)

    deployments[index].Release = JsonConvert.DeserializeObject<Release>(reljsons[index]);

首先,您列出未完成的任务。然后你等待它,你会得到一组结果(reljson's)。然后你必须反序列化它们并分配给Release

通过使用await Task.WhenAll(),您可以同时等待所有任务,因此您应该会看到性能提升。

如果有错别字请告诉我,我没有编译这段代码。

【讨论】:

谢谢!我采用这种方法,对结果进行了一些小的调整。我没有遍历所有 Deployment,而是创建了一个 Release 列表,然后通过其 Id 将正确的 Release 映射到正确的 Deployment。 @ZiNNED 我很高兴它成功了。我自己对这个 for 循环并不感到自豪,所以我很高兴你想出了一个更好的方法。【参考方案3】:

Fcin 建议启动所有任务,等待它们全部完成,然后开始反序列化获取的数据。

但是,如果第一个任务已经完成,但第二个任务没有完成,并且第二个任务在内部等待,则第一个任务可能已经开始反序列化。这将缩短您的进程空闲等待的时间。

所以而不是:

var deplTasks = deployments.Select(d => GetJson($"BASEURLrelease/releases/d.ReleaseId"));
var reljsons = await Task.WhenAll(deplTasks);
for(var index = 0; index < deployments.Count; index++)

    deployments[index].Release = JsonConvert.DeserializeObject<Release>(reljsons[index]);

我建议进行以下细微更改:

// async fetch the Release data of Deployment:
private async Task<Release> FetchReleaseDataAsync(Deployment deployment)

    var reljson = await GetJson($"BASEURLrelease/releases/deployment.ReleaseId");
    return JsonConvert.DeserializeObject<Release>(reljson);


// async fill the Release data of Deployment:
private async Task FillReleaseDataAsync(Deployment deployment)

    deployment.Release = await FetchReleaseDataAsync(deployment);

那么你的程序类似于Fcin建议的解决方案:

IEnumerable<Task> tasksFillDeploymentWithReleaseData = deployments.
    .Select(deployment => FillReleaseDataAsync(deployment)
    .ToList();
await Task.WhenAll(tasksFillDeploymentWithReleaseData);

现在如果第一个任务在获取发布数据时必须等待,则第二个任务开始,第三个任务开始,以此类推。如果第一个任务已经完成获取发布数据,但其他任务正在等待它们的发布数据,第一个任务任务已经开始反序列化它并将结果分配给deployment.Release,之后第一个任务完成。

例如,如果第 7 个任务获得了数据,但第 2 个任务仍在等待,则第 7 个任务可以反序列化并将数据分配给部署。释放。任务 7 已完成。

这会一直持续到所有任务完成为止。使用这种方法可以减少等待时间,因为一旦一个任务有数据,它就会被安排开始反序列化

【讨论】:

这是一个比我更好的解决方案:)

以上是关于在for-each-loop中等待异步调用[重复]的主要内容,如果未能解决你的问题,请参考以下文章

等待所有异步请求在循环内完成[重复]

JavaScript在if语句中等待异步函数[重复]

每个数组元素的异步调用并等待完成[重复]

在 ExpressJS 响应中返回异步等待 try/catch 抛出错误 [重复]

异步 - 等待 JavaScript:无法从错误对象中捕获错误详细信息 [重复]

等待先前的异步功能完成[重复]