我尝试使用从.txt
文件中读取的mutliple输入网址调用Google PageSpeed Insights API,并将结果输出到.csv
。
我写了一个控制台应用程序试图解除这些请求,然后当他们回来将它们添加到列表中时,当它们全部完成时,将list
写入.csv
} file(尝试立即将响应写入.csv
时,async有点疯狂。)
我的代码在下面,远非优化。我来自JavaScript背景,我通常不使用网络工作者或任何其他托管新线程,所以我试图在C#中做同样的事情。
WebRequest
并将它们写入集合(或输出文件)而不使用多个线程并让它们全部并行运行,而不必在处理之前等待每个请求返回下一个?BackgroundWorker
,那么Clean Code这样做的方式是什么?static void Main(string[] args)
{
Console.WriteLine("Begin Google PageSpeed Insights!");
appMode = ConfigurationManager.AppSettings["ApplicationMode"];
var inputFilePath = READ_WRITE_PATH + ConfigurationManager.AppSettings["InputFile"];
var outputFilePath = READ_WRITE_PATH + ConfigurationManager.AppSettings["OutputFile"];
var inputLines = File.ReadAllLines(inputFilePath).ToList();
if (File.Exists(outputFilePath))
{
File.Delete(outputFilePath);
}
List<string> outputCache = new List<string>();
foreach (var line in inputLines)
{
var requestDataFromPsi = CallPsiForPrimaryStats(line);
Console.WriteLine($"Got response of {requestDataFromPsi.Result}");
outputCache.Add(requestDataFromPsi.Result);
}
var writeTask = WriteCharacters(outputCache, outputFilePath);
writeTask.Wait();
Console.WriteLine("End Google PageSpeed Insights");
}
private static async Task<string> CallPsiForPrimaryStats(string url)
{
HttpWebRequest myReq = (HttpWebRequest)WebRequest.Create($"https://www.googleapis.com/pagespeedonline/v2/runPagespeed?url={url}&strategy=mobile&key={API_KEY}");
myReq.Method = WebRequestMethods.Http.Get;
myReq.Timeout = 60000;
myReq.Proxy = null;
myReq.ContentType = "application/json";
Task<WebResponse> task = Task.Factory.FromAsync(
myReq.BeginGetResponse,
asyncResult => myReq.EndGetResponse(asyncResult),
(object)null);
return await task.ContinueWith(t => ReadStreamFromResponse(t.Result));
}
private static string ReadStreamFromResponse(WebResponse response)
{
using (Stream responseStream = response.GetResponseStream())
using (StreamReader sr = new StreamReader(responseStream))
{
string jsonResponse = sr.ReadToEnd();
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonResponse);
var psiResp = new PsiResponse()
{
Url = jsonObj.id,
SpeedScore = jsonObj.ruleGroups.SPEED.score,
UsabilityScore = jsonObj.ruleGroups.USABILITY.score,
NumberResources = jsonObj.pageStats.numberResources,
NumberHosts = jsonObj.pageStats.numberHosts,
TotalRequestBytes = jsonObj.pageStats.totalRequestBytes,
NumberStaticResources = jsonObj.pageStats.numberStaticResources,
HtmlResponseBytes = jsonObj.pageStats.htmlResponseBytes,
CssResponseBytes = jsonObj.pageStats.cssResponseBytes,
ImageResponseBytes = jsonObj.pageStats.imageResponseBytes,
JavascriptResponseBytes = jsonObj.pageStats.javascriptResponseBytes,
OtherResponseBytes = jsonObj.pageStats.otherResponseBytes,
NumberJsResources = jsonObj.pageStats.numberJsResources,
NumberCssResources = jsonObj.pageStats.numberCssResources,
};
return CreateOutputString(psiResp);
}
}
static async Task WriteCharacters(List<string> inputs, string outputFilePath)
{
using (StreamWriter fileWriter = new StreamWriter(outputFilePath))
{
await fileWriter.WriteLineAsync(TABLE_HEADER);
foreach (var input in inputs)
{
await fileWriter.WriteLineAsync(input);
}
}
}
private static string CreateOutputString(PsiResponse psiResponse)
{
var stringToWrite = "";
foreach (var prop in psiResponse.GetType().GetProperties())
{
stringToWrite += $"{prop.GetValue(psiResponse, null)},";
}
Console.WriteLine(stringToWrite);
return stringToWrite;
}
问题是这仍然很慢。原来花了20分钟,重构后仍然需要20分钟。它似乎被某个地方限制,可能是谷歌在PageSpeed API上。我测试了它,打电话给https://www.google.com/,https://www.yahoo.com/,https://www.bing.com/和其他18个人,它也运行缓慢,一次只能处理大约5个请求的瓶颈。我尝试重构运行5个测试URL,然后写入文件并重复,但它只是略微加快了这个过程。
static void Main(string[] args) { MainAsync(args).Wait(); }
static async Task MainAsync(string[] args)
{
Console.WriteLine("Begin Google PageSpeed Insights!");
appMode = ConfigurationManager.AppSettings["ApplicationMode"];
var inputFilePath = READ_WRITE_PATH + ConfigurationManager.AppSettings["InputFile"];
var outputFilePath = READ_WRITE_PATH + ConfigurationManager.AppSettings["OutputFile"];
var inputLines = File.ReadAllLines(inputFilePath).ToList();
if (File.Exists(outputFilePath))
{
File.Delete(outputFilePath);
}
var tasks = inputLines.Select(line => CallPsiForPrimaryStats(line));
var outputCache = await Task.WhenAll(tasks);
await WriteCharacters(outputCache, outputFilePath);
Console.WriteLine("End Google PageSpeed Insights");
}
private static async Task<string> CallPsiForPrimaryStats(string url)
{
HttpWebRequest myReq = (HttpWebRequest)WebRequest.Create($"https://www.googleapis.com/pagespeedonline/v2/runPagespeed?url={url}&strategy=mobile&key={API_KEY}");
myReq.Method = WebRequestMethods.Http.Get;
myReq.Timeout = 60000;
myReq.Proxy = null;
myReq.ContentType = "application/json";
Console.WriteLine($"Start call: {url}");
// Try using `HttpClient()` later
//var myReq2 = new HttpClient();
//await myReq2.GetAsync($"https://www.googleapis.com/pagespeedonline/v2/runPagespeed?url={url}&strategy=mobile&key={API_KEY}");
Task<WebResponse> task = Task.Factory.FromAsync(
myReq.BeginGetResponse,
myReq.EndGetResponse,
(object)null);
var result = await task;
return ReadStreamFromResponse(result);
}
private static string ReadStreamFromResponse(WebResponse response)
{
using (Stream responseStream = response.GetResponseStream())
using (StreamReader sr = new StreamReader(responseStream))
{
string jsonResponse = sr.ReadToEnd();
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonResponse);
var psiResp = new PsiResponse()
{
Url = jsonObj.id,
SpeedScore = jsonObj.ruleGroups.SPEED.score,
UsabilityScore = jsonObj.ruleGroups.USABILITY.score,
NumberResources = jsonObj.pageStats.numberResources,
NumberHosts = jsonObj.pageStats.numberHosts,
TotalRequestBytes = jsonObj.pageStats.totalRequestBytes,
NumberStaticResources = jsonObj.pageStats.numberStaticResources,
HtmlResponseBytes = jsonObj.pageStats.htmlResponseBytes,
CssResponseBytes = jsonObj.pageStats.cssResponseBytes,
ImageResponseBytes = jsonObj.pageStats.imageResponseBytes,
JavascriptResponseBytes = jsonObj.pageStats.javascriptResponseBytes,
OtherResponseBytes = jsonObj.pageStats.otherResponseBytes,
NumberJsResources = jsonObj.pageStats.numberJsResources,
NumberCssResources = jsonObj.pageStats.numberCssResources,
};
return CreateOutputString(psiResp);
}
}
static async Task WriteCharacters(IEnumerable<string> inputs, string outputFilePath)
{
using (StreamWriter fileWriter = new StreamWriter(outputFilePath))
{
await fileWriter.WriteLineAsync(TABLE_HEADER);
foreach (var input in inputs)
{
await fileWriter.WriteLineAsync(input);
}
}
}
private static string CreateOutputString(PsiResponse psiResponse)
{
var stringToWrite = "";
foreach (var prop in psiResponse.GetType().GetProperties())
{
stringToWrite += $"{prop.GetValue(psiResponse, null)},";
}
Console.WriteLine(stringToWrite);
return stringToWrite;
}
答案 0 :(得分:4)
我可以运行这些多个WebRequest并将它们写入集合(或输出文件)而不使用多个线程并让它们全部并行运行,而不必等待每个请求在处理下一个请求之前返回吗? / p>
是;您正在寻找的是异步并发,它使用Task.WhenAll
。
使用回调是否有更简洁的方法来执行此操作?
async
/ await
比回调更干净。 JavaScript已从回调转换为承诺(类似于C#中的Task<T>
),转移到async
/ await
(非常类似于C#中的async
/ await
。两种语言中最干净的解决方案现在是async
/ await
。
但是,C#中存在一些问题,主要是由于向后兼容性。
1)在异步控制台应用中,您需要阻止Main
方法。一般来说,这应该是阻止异步代码的唯一时间:
static void Main(string[] args) { MainAsync(args).Wait(); }
static async Task MainAsync(string[] args)
{
一旦有async MainAsync
方法,就可以使用Task.WhenAll
进行异步并发:
...
var tasks = inputLines.Select(line => CallPsiForPrimaryStats(line));
var outputCache = await Task.WhenAll(tasks);
await WriteCharacters(outputCache, outputFilePath);
...
2)你不应该使用ContinueWith
;它是一个低级,危险的API。请改用await
:
private static async Task<string> CallPsiForPrimaryStats(string url)
{
...
Task<WebResponse> task = Task.Factory.FromAsync(
myReq.BeginGetResponse,
myReq.EndGetResponse,
(object)null);
var result = await task;
return ReadStreamFromResponse(result);
}
3)通常有更多“异步友好”类型可用。在这种情况下,请考虑使用HttpClient
代替HttpWebRequest
;你会发现你的代码清理了很多。
答案 1 :(得分:2)
我可以运行这些多个WebRequest并将它们写入集合(或输出文件)而不使用多个线程并让它们全部并行运行,而不必等待每个请求在处理下一个请求之前返回吗? / p>
当然可以。
使用回调是否有更简洁的方法来执行此操作?
您可以随时遍历输入行并获取所有正在运行的任务的集合。
var resultTask = Task.WhenAll(
inputLines.Select(line => CallPsiForPrimaryStats(line)).ToArray());
这类似于在Javascript中使用Q
库进行承诺。使用.Net任务,主机可以并行启动尽可能多的进程。
resultTask
将是您可以使用的结果的集合,就像您的outputCache
一样。
在上面添加的代码中,循环中对.Result
的调用将是同步的。没有什么是并行发生的。等待所有这些时要小心,在它全部传回之前你可能会耗尽内存!当它们返回时,可能值得将此流式传输到文件中,并且具有信号量或锁定会阻止它们立即写入流中。
此外,我认为WebClient课程现在比手工HttpWebRequest
更加惯用。
如果需要线程或BackgroundWorkers,那么干净代码的做法是什么?
这是Task
库和.Net的异步堆栈之美。你不应该对线程做任何事情。
了解async/await
类型通话和synchronous
通话之间的区别非常重要。您在方法声明中看到async
的任何地方和正文中的await
意味着代码释放当前同步线程以执行其他工作,例如启动更多任务。当您看到.Result
或.Wait()
这些是同步的,因此阻止主同步线程。这意味着没有简单并行的能力。