I / O绑定任务的Parallel.ForEach比Task.WaitAll快吗?

时间:2019-09-25 16:46:08

标签: c# asynchronous async-await task parallel.foreach

我的程序有两个版本,它们向Web服务器提交约3000个HTTP GET请求。

第一版基于我读过的here。该解决方案对我来说很有意义,因为发出Web请求是I / O绑定的工作,并且与Task.WhenAll或Task.WaitAll一起使用async / await意味着您可以一次提交100个请求,然后等待所有请求请先完成此操作,然后再提交下一个100个请求,以免使Web服务器陷入困境。我很惊讶地看到这个版本在大约12分钟内完成了所有工作,比我预期的要慢得多。

第二个版本在Parallel.ForEach循环中提交所有3000个HTTP GET请求。我使用.Result等待每个请求完成,然后该循环迭代中的其余逻辑可以执行。我认为这将是效率低得多的解决方案,因为使用线程并行执行任务通常更适合执行CPU约束的工作,但是令我惊讶的是,这个版本在大约3分钟内完成了所有工作!

我的问题是为什么Parallel.ForEach版本更快?令我感到意外的是,当我对不同 API /网络服务器应用相同的两种技术时,我的代码的版本1 实际上比版本2快6个分钟-这是我所期望的。两种不同版本的性能是否与Web服务器处理流量有关?

您可以在下面看到我的代码的简化版本:

private async Task<ObjectDetails> TryDeserializeResponse(HttpResponseMessage response)
{
    try
    {
        using (Stream stream = await response.Content.ReadAsStreamAsync())
        using (StreamReader readStream = new StreamReader(stream, Encoding.UTF8))
        using (JsonTextReader jsonTextReader = new JsonTextReader(readStream))
        {
            JsonSerializer serializer = new JsonSerializer();
            ObjectDetails objectDetails = serializer.Deserialize<ObjectDetails>(
                jsonTextReader);
            return objectDetails;
        }
    }
    catch (Exception e)
    {
        // Log exception
        return null;
    }
}

private async Task<HttpResponseMessage> TryGetResponse(string urlStr)
{
    try
    {
        HttpResponseMessage response = await httpClient.GetAsync(urlStr)
            .ConfigureAwait(false);
        if (response.StatusCode != HttpStatusCode.OK)
        {
            throw new WebException("Response code is "
                + response.StatusCode.ToString() + "... not 200 OK.");
        }
        return response;
    }
    catch (Exception e)
    {
        // Log exception
        return null;
    }
}

private async Task<ListOfObjects> GetObjectDetailsAsync(string baseUrl, int id)
{
    string urlStr = baseUrl + @"objects/id/" + id + "/details";

    HttpResponseMessage response = await TryGetResponse(urlStr);

    ObjectDetails objectDetails = await TryDeserializeResponse(response);

    return objectDetails;
}

// With ~3000 objects to retrieve, this code will create 100 API calls
// in parallel, wait for all 100 to finish, and then repeat that process
// ~30 times. In other words, there will be ~30 batches of 100 parallel
// API calls.
private Dictionary<int, Task<ObjectDetails>> GetAllObjectDetailsInBatches(
    string baseUrl, Dictionary<int, MyObject> incompleteObjects)
{
    int batchSize = 100;
    int numberOfBatches = (int)Math.Ceiling(
        (double)incompleteObjects.Count / batchSize);
    Dictionary<int, Task<ObjectDetails>> objectTaskDict
        = new Dictionary<int, Task<ObjectDetails>>(incompleteObjects.Count);

    var orderedIncompleteObjects = incompleteObjects.OrderBy(pair => pair.Key);

    for (int i = 0; i < 1; i++)
    {
        var batchOfObjects = orderedIncompleteObjects.Skip(i * batchSize)
            .Take(batchSize);
        var batchObjectsTaskList = batchOfObjects.Select(
            pair => GetObjectDetailsAsync(baseUrl, pair.Key));
        Task.WaitAll(batchObjectsTaskList.ToArray());
        foreach (var objTask in batchObjectsTaskList)
            objectTaskDict.Add(objTask.Result.id, objTask);
    }

    return objectTaskDict;
}

public void GetObjectsVersion1()
{
    string baseUrl = @"https://mywebserver.com:/api";

    // GetIncompleteObjects is not shown, but it is not relevant to
    // the question
    Dictionary<int, MyObject> incompleteObjects = GetIncompleteObjects();

    Dictionary<int, Task<ObjectDetails>> objectTaskDict
        = GetAllObjectDetailsInBatches(baseUrl, incompleteObjects);

    foreach (KeyValuePair<int, MyObject> pair in incompleteObjects)
    {
        ObjectDetails objectDetails = objectTaskDict[pair.Key].Result
            .objectDetails;

        // Code here that copies fields from objectDetails to pair.Value
        // (the incompleteObject)

        AllObjects.Add(pair.Value);
    };
}

public void GetObjectsVersion2()
{
    string baseUrl = @"https://mywebserver.com:/api";

    // GetIncompleteObjects is not shown, but it is not relevant to
    // the question
    Dictionary<int, MyObject> incompleteObjects = GetIncompleteObjects();

    Parallel.ForEach(incompleteHosts, pair =>
    {
        ObjectDetails objectDetails = GetObjectDetailsAsync(
            baseUrl, pair.Key).Result.objectDetails;

        // Code here that copies fields from objectDetails to pair.Value
        // (the incompleteObject)

        AllObjects.Add(pair.Value);
    });
}

3 个答案:

答案 0 :(得分:1)

简而言之:

  • Parallel.Foreach()对于与CPU绑定的任务最有用。
  • Task.WaitAll()对于IO绑定任务更有用。

因此,在您的情况下,您是从Web服务器(即IO)获取信息。如果异步方法正确实现,则不会阻塞任何线程。 (它将使用IO完成端口等待)这样,线程可以执行其他操作。

通过同步运行GetObjectDetailsAsync(baseUrl, pair.Key).Result的异步方法,它将阻塞线程。因此,线程池将被等待的线程淹没。

所以我认为Task解决方案会更合适。

答案 1 :(得分:0)

https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreach?view=netframework-4.8

基本上,parralel foreach允许迭代并行运行,因此您不必限制迭代以串行方式运行,在不受线程约束的主机上,这会导致吞吐量提高

答案 2 :(得分:0)

Parallel.ForEach运行更快的可能原因是,它会产生节流的副作用。最初,x个线程正在处理前x个元素(其中,可用核数为x),并且可能会根据内部启发法逐渐添加更多线程。限制IO操作是一件好事,因为它可以保护网络和处理请求的服务器不致负担过重。由于许多原因,您通过100个批次的请求提出的另一种简易的节流方法远非理想,其中之一就是100个并发请求很多。另一个问题是,一个长时间运行的操作可能会将批处理的完成推迟到其他99个操作完成后的很长时间。

请注意,Parallel.ForEach对于并行化IO操作也不理想。它恰好比其他方法执行得更好,始终浪费内存。有关更好的方法,请参见:How to limit the amount of concurrent async I/O operations?