您是否需要后台工作程序或多个线程来触发多个Async HttpWebRequests?

时间:2017-07-12 13:26:23

标签: c# multithreading asynchronous httpwebrequest backgroundworker

总体目标

我尝试使用从.txt文件中读取的mutliple输入网址调用Google PageSpeed Insights API,并将结果输出到.csv

我尝试了什么

我写了一个控制台应用程序试图解除这些请求,然后当他们回来将它们添加到列表中时,当它们全部完成时,将list写入.csv } file(尝试立即将响应写入.csv时,async有点疯狂。)

我的代码在下面,远非优化。我来自JavaScript背景,我通常不使用网络工作者或任何其他托管新线程,所以我试图在C#中做同样的事情。

  1. 我可以运行这些多个WebRequest并将它们写入集合(或输出文件)而不使用多个线程并让它们全部并行运行,而不必在处理之前等待每个请求返回下一个?
  2. 使用回调
  3. 有更简洁的方法吗?
  4. 如果需要线程或BackgroundWorker,那么Clean Code这样做的方式是什么?
  5. 初始示例代码

    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;
    }
    

    更新:从Stephen Cleary Tips重构后

    问题是这仍然很慢。原来花了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;
    }
    

2 个答案:

答案 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()这些是同步的,因此阻止主同步线程。这意味着没有简单并行的能力。