【发布时间】:2019-09-25 16:46:08
【问题描述】:
我的程序有两个版本,它们向 Web 服务器提交约 3000 个 HTTP GET 请求。
第一个版本基于我阅读的 here。该解决方案对我来说很有意义,因为发出 Web 请求是受 I/O 限制的工作,并且将 async/await 与 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);
});
}
【问题讨论】:
-
在某些时候您没有使用 ConfigureAwait(false)(请参阅
GetObjectDetailsAsync),这将对性能产生很大影响,因为代码正在等待同步。 -
此外,这段代码在哪个上下文中运行也会很有趣。 WinForms/WPF/asp.net/Console/...?
-
@SirRufo 啊,是的,我将在那里添加 ConfigureAwait(false) 以查看它如何影响性能。它是一个控制台应用程序(.Net Framework 4.6.1)
-
@SirRufo 在 GetObjectDetailsAsync 中添加 ConfigureAwait(false) 似乎不会影响性能。
-
是的,控制台应用程序根本没有同步上下文,因此没有会影响性能的同步。这就是我想知道代码在哪种应用程序中运行的原因
标签: c# asynchronous async-await task parallel.foreach