C# 对于I/O绑定的任务,Parallel.ForEach比Task.WaitAll快?

C# 对于I/O绑定的任务,Parallel.ForEach比Task.WaitAll快?,c#,asynchronous,async-await,task,parallel.foreach,C#,Asynchronous,Async Await,Task,Parallel.foreach,我有两个版本的程序,可以向web服务器提交约3000个HTTP GET请求 第一个版本是根据我读到的内容编写的。这个解决方案对我来说很有意义,因为发出web请求是I/O绑定的工作,而async/wait与Task.WhenAll或Task.WaitAll一起使用意味着您可以一次提交100个请求,然后等待它们全部完成,然后再提交下100个请求,这样您就不会使web服务器陷入困境。我惊讶地看到,这个版本在12分钟内完成了所有的工作,比我预期的慢多了 第二个版本在Parallel.ForEach循环中

我有两个版本的程序,可以向web服务器提交约3000个HTTP GET请求

第一个版本是根据我读到的内容编写的。这个解决方案对我来说很有意义,因为发出web请求是I/O绑定的工作,而async/wait与Task.WhenAll或Task.WaitAll一起使用意味着您可以一次提交100个请求,然后等待它们全部完成,然后再提交下100个请求,这样您就不会使web服务器陷入困境。我惊讶地看到,这个版本在12分钟内完成了所有的工作,比我预期的慢多了

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

我的问题是为什么Parallel.ForEach版本更快?这是一个额外的惊喜,因为当我对不同的API/web服务器应用相同的两种技术时,我的代码的版本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);
    });
}
专用异步任务TryDeserializeResponse(HttpResponseMessage响应)
{
尝试
{
使用(Stream=await response.Content.ReadAsStreamAsync())
使用(StreamReader readStream=newstreamreader(stream,Encoding.UTF8))
使用(JsonTextReader JsonTextReader=newjsontextreader(readStream))
{
JsonSerializer serializer=新的JsonSerializer();
ObjectDetails ObjectDetails=序列化程序。反序列化(
jsonTextReader);
返回对象详细信息;
}
}
捕获(例外e)
{
//日志异常
返回null;
}
}
专用异步任务TryGetResponse(字符串urlStr)
{
尝试
{
HttpResponseMessage response=等待httpClient.GetAsync(urlStr)
.配置等待(错误);
if(response.StatusCode!=HttpStatusCode.OK)
{
抛出新的WebException(“响应代码为”
+response.StatusCode.ToString()+“…不正常。”);
}
返回响应;
}
捕获(例外e)
{
//日志异常
返回null;
}
}
专用异步任务GetObjectDetailsAsync(字符串baseUrl,int id)
{
字符串urlStr=baseUrl+@“objects/id/”+id+“/details”;
HttpResponseMessage response=等待TryGetResponse(urlStr);
ObjectDetails ObjectDetails=等待TryDeserializeResponse(响应);
返回对象详细信息;
}
//要检索约3000个对象,此代码将创建100个API调用
//同时,等待所有100个完成,然后重复该过程
//大约30次。换言之,将有约30批100个平行样品
//API调用。
专用字典GetAllObjectDetailsInBatch(
字符串baseUrl,字典不完整对象)
{
int batchSize=100;
int numberOfBatches=(int)数学上限(
(双精度)不完整对象。计数/批量大小);
字典对象任务dict
=新字典(UncompleteObjects.Count);
var orderedCompleteObjects=不完全对象.OrderBy(pair=>pair.Key);
对于(int i=0;i<1;i++)
{
var batchOfObjects=OrderedCompleteObjects.Skip(i*batchSize)
.取(批量大小);
var batchObjectsTaskList=batchOfObject。选择(
pair=>GetObjectDetailsAsync(baseUrl,pair.Key));
Task.WaitAll(batchObjectsTaskList.ToArray());
foreach(batchObjectsTaskList中的var objTask)
objectTaskDict.Add(objTask.Result.id,objTask);
}
返回objectTaskDict;
}
public void GetObjectsVersion1()
{
字符串baseUrl=@“https://mywebserver.com:/api";
//GetUncompleteObjects未显示,但它与
//问题
Dictionary incompleteObjects=GetIncompleteObjects();
字典对象任务dict
=GetAllObjectDetailsInBatches(baseUrl,不完整对象);
foreach(不完全对象中的KeyValuePair对)
{
ObjectDetails ObjectDetails=objectTaskDict[pair.Key]。结果
。详情;
//此处的代码将字段从objectDetails复制到pair.Value
//(不完全对象)
AllObjects.Add(pair.Value);
};
}
public void GetObjectsVersion2()
{
字符串baseUrl=@“https://mywebserver.com:/api";
//GetUncompleteObjects未显示,但它与
//问题
Dictionary incompleteObjects=GetIncompleteObjects();
Parallel.ForEach(不完全主机,对=>
{
ObjectDetails ObjectDetails=GetObjectDetailsAsync(
baseUrl,pair.Key)。Result.objectDetails;
//此处的代码将字段从objectDetails复制到pair.Value
//(不完全对象)
AllObjects.Add(pair.Value);
});
}

基本上,parralel foreach允许迭代并行运行,因此不限制迭代在不受线程约束的主机上串行运行。这将有助于提高吞吐量简而言之:

  • Parallel.Foreach()
    对于CPU限制的任务最有用
  • Task.WaitAll()
    对于IO绑定的任务更有用
因此,在您的情况下,您从Web服务器获取信息,这就是IO。如果异步方法实现正确,它将不会阻塞任何线程。(它将使用IO完成端口等待)这样线程就可以做其他事情