在for-each-loop中等待异步调用

时间:2018-09-25 09:06:41

标签: c# foreach async-await

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

public static async Task<List<Deployment>> GetDeployments()
{
    try
    {
        var depjson     = await GetJson($"{BASEURL}release/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($"{BASEURL}release/releases/{deployment.ReleaseId}");
            deployment.Release = JsonConvert.DeserializeObject<Release>(reljson);
        }

        return deployments;
    }
    catch (Exception)
    {
        throw;
    }
}

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

我希望您能提出有关如何更快地实现此方法的任何建议,并尽可能避免在foreach循环中出现await-。

3 个答案:

答案 0 :(得分:1)

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

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

所以代替:

var deplTasks = deployments.Select(d => GetJson($"{BASEURL}release/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($"{BASEURL}release/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);

现在,如果第一个任务在获取释放数据时必须等待,则第二个任务开始,第三个任务开始,依此类推。如果第一个任务已经完成了获取释放数据,但其他任务正在等待其释放数据,则第一个任务任务已经开始反序列化,并将结果分配给部署。发布后,第一个任务完成。

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

此过程一直持续到所有任务完成为止。使用此方法可以减少等待时间,因为一旦一项任务有其数据,就计划开始反序列化

答案 1 :(得分:0)

您现在所做的一切都没有错。但是,有一种方法可以立即调用所有任务,而不是等待单个任务,然后处理它,然后等待另一个任务。

这是您可以打开它的方式:

wait for one -> process -> wait for one -> process ...

进入

wait for all -> process -> done

转换此:

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

收件人:

var deplTasks = deployments.Select(d => GetJson($"{BASEURL}release/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)。然后,您必须将它们反序列化并分配给Release

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

让我知道是否有错别字,我没有编译这段代码。

答案 2 :(得分:-1)

如果我正确理解您的意思,并且您想将var reljson = await GetJson设为并行:

尝试一下:

Parallel.ForEach(deployments, (deployment) =>
{
    var reljson = await GetJson($"{BASEURL}release/releases/{deployment.ReleaseId}");
    deployment.Release = JsonConvert.DeserializeObject<Release>(reljson);
});

您可以限制并行执行的次数,例如:

Parallel.ForEach(
    deployments,
    new ParallelOptions { MaxDegreeOfParallelism = 4 },
    (deployment) =>
{
    var reljson = await GetJson($"{BASEURL}release/releases/{deployment.ReleaseId}");
    deployment.Release = JsonConvert.DeserializeObject<Release>(reljson);
});

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

Parallel.ForEach(deployments, (deployment, state) =>
{
    var reljson = await GetJson($"{BASEURL}release/releases/{deployment.ReleaseId}");
    deployment.Release = JsonConvert.DeserializeObject<Release>(reljson);
    if (noFurtherProcessingRequired) state.Break();
});