我的程序有两个版本,它们向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);
});
}
答案 0 :(得分:1)
简而言之:
Parallel.Foreach()
对于与CPU绑定的任务最有用。Task.WaitAll()
对于IO绑定任务更有用。因此,在您的情况下,您是从Web服务器(即IO)获取信息。如果异步方法正确实现,则不会阻塞任何线程。 (它将使用IO完成端口等待)这样,线程可以执行其他操作。
通过同步运行GetObjectDetailsAsync(baseUrl, pair.Key).Result
的异步方法,它将阻塞线程。因此,线程池将被等待的线程淹没。
所以我认为Task解决方案会更合适。
答案 1 :(得分:0)
基本上,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?