C# 对于I/O绑定的任务,Parallel.ForEach比Task.WaitAll快?
我有两个版本的程序,可以向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服务器处理流量的方式有关 您可以在下面看到我的代码的简化版本: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循环中
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允许迭代并行运行,因此不限制迭代在不受线程约束的主机上串行运行。这将有助于提高吞吐量简而言之:
对于CPU限制的任务最有用Parallel.Foreach()
对于IO绑定的任务更有用Task.WaitAll()