c#.net 4.5 async / multithread?

时间:2012-07-24 20:30:58

标签: c# multithreading .net-4.5

我正在编写一个从网页上抓取数据的C#控制台应用程序。

此应用程序将转到大约8000个网页并刮取数据(每页上的数据格式相同)。

我现在没有异步方法,也没有多线程。

但是,我需要它更快。它只使用了大约3%-6%的CPU,我想是因为它花时间等待下载html。(WebClient.DownloadString(url))

这是我程序的基本流程

DataSet alldata;

foreach(var url in the8000urls)
{
    // ScrapeData downloads the html from the url with WebClient.DownloadString
    // and scrapes the data into several datatables which it returns as a dataset.
    DataSet dataForOnePage = ScrapeData(url);

    //merge each table in dataForOnePage into allData
}

// PushAllDataToSql(alldata);

我一直试图多线程,但我不确定如何正确开始。我正在使用.net 4.5而且我的理解是异步并且等待4.5以使这更容易编程,但我仍然有点迷失。

我的想法是继续制作与此行异步的新线程

DataSet dataForOnePage = ScrapeData(url);

然后当每个人完成时,运行

//merge each table in dataForOnePage into allData

任何人都可以指出我正确的方向,如何在.net 4.5 c#中使该行异步,然后让我的合并方法运行完成?

谢谢。

编辑:这是我的ScrapeData方法:

public static DataSet GetProperyData(CookieAwareWebClient webClient, string pageid)
{
    var dsPageData = new DataSet();

    // DOWNLOAD HTML FOR THE REO PAGE AND LOAD IT INTO AN HTMLDOCUMENT
    string url = @"https://domain.com?&id=" + pageid + @"restofurl";
    string html = webClient.DownloadString(url);
    var doc = new HtmlDocument();
    doc.LoadHtml(html );

    // A BUNCH OF PARSING WITH HTMLAGILITY AND STORING IN dsPageData 
    return dsPageData ;
}

4 个答案:

答案 0 :(得分:41)

如果您想使用asyncawait关键字(虽然您不必使用,但它们确实在.NET 4.5中更容易),您首先要更改{ {1}}使用ScrapeData关键字返回Task<T> instance的方法,如下所示:

async

请注意,您可能希望远离async Task<DataSet> ScrapeDataAsync(Uri url) { // Create the HttpClientHandler which will handle cookies. var handler = new HttpClientHandler(); // Set cookies on handler. // Await on an async call to fetch here, convert to a data // set and return. var client = new HttpClient(handler); // Wait for the HttpResponseMessage. HttpResponseMessage response = await client.GetAsync(url); // Get the content, await on the string content. string content = await response.Content.ReadAsStringAsync(); // Process content variable here into a data set and return. DataSet ds = ...; // Return the DataSet, it will return Task<DataSet>. return ds; } 类,因为它在异步操作中不支持WebClient。 .NET 4.5中更好的选择是HttpClient class。我选择使用上面的Task<T>。另外,请查看HttpClientHandler class,特别是您将用于向每个请求发送Cookie的CookieContainer property

但是,这意味着您很可能不得不使用HttpClient关键字等待另一个异步操作,在这种情况下,很可能是下载这页纸。您必须定制下载数据的调用以使用异步版本,并使用await

一旦完成,您通常会在其上调用await,但在这种情况下您无法执行此操作,因为您会对变量await。在这种情况下,您正在运行循环,因此每次迭代都会重置变量。在这种情况下,最好将await存储在一个数组中,如下所示:

Task<T>

将数据合并到DataSet alldata = ...; var tasks = new List<Task<DataSet>>(); foreach(var url in the8000urls) { // ScrapeData downloads the html from the url with // WebClient.DownloadString // and scrapes the data into several datatables which // it returns as a dataset. tasks.Add(ScrapeDataAsync(url)); } 。为此,您要调用返回的allData实例上的ContinueWith method,并执行将数据添加到Task<T>的任务:

allData

然后,您可以使用WhenAll methodDataSet alldata = ...; var tasks = new List<Task<DataSet>>(); foreach(var url in the8000urls) { // ScrapeData downloads the html from the url with // WebClient.DownloadString // and scrapes the data into several datatables which // it returns as a dataset. tasks.Add(ScrapeDataAsync(url).ContinueWith(t => { // Lock access to the data set, since this is // async now. lock (allData) { // Add the data. } }); } 上的Task class等待所有任务:

await

但请注意,您有一个// After your loop. await Task.WhenAll(tasks); // Process allData foreach需要IEnumerable<T>个实施。这是一个很好的指标,它适合使用LINQ,它是:

WhenAll

如果您愿意,也可以选择不使用查询语法,在这种情况下无关紧要。

请注意,如果包含方法未标记为DataSet alldata; var tasks = from url in the8000Urls select ScrapeDataAsync(url).ContinueWith(t => { // Lock access to the data set, since this is // async now. lock (allData) { // Add the data. } }); await Task.WhenAll(tasks); // Process allData (因为您在控制台应用程序中并且必须在应用程序终止之前等待结果),那么您只需调用Wait method即可致电async时返回Task

WhenAll

即,您需要将// This will block, waiting for all tasks to complete, all // tasks will run asynchronously and when all are done, then the // code will continue to execute. Task.WhenAll(tasks).Wait(); // Process allData. 个实例收集到一个序列中,然后在处理Task之前等待整个序列。

但是,如果可以的话,我建议在将数据合并到allData之前尝试处理数据;除非数据处理需要整个 allData,否则当您获得 时,您将获得更多的数据,从而获得更多性能提升回来,而不是等待所有回来。

答案 1 :(得分:11)

你也可以使用TPL Dataflow,这非常适合这类问题。

在这种情况下,您构建一个&#34;数据流网格&#34;然后你的数据流过它。

这个实际上更像是一个管道,而不是一个&#34;网格&#34;。我分三步:从URL下载(字符串)数据;将(字符串)数据解析为HTML,然后解析为DataSet;并将DataSet合并到主DataSet

首先,我们创建将进入网格的块:

DataSet allData;
var downloadData = new TransformBlock<string, string>(
  async pageid =>
  {
    System.Net.WebClient webClient = null;
    var url = "https://domain.com?&id=" + pageid + "restofurl";
    return await webClient.DownloadStringTaskAsync(url);
  },
  new ExecutionDataflowBlockOptions
  {
    MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded,
  });
var parseHtml = new TransformBlock<string, DataSet>(
  html =>
  {
    var dsPageData = new DataSet();
    var doc = new HtmlDocument();
    doc.LoadHtml(html);

    // HTML Agility parsing

    return dsPageData;
  },
  new ExecutionDataflowBlockOptions
  {
    MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded,
  });
var merge = new ActionBlock<DataSet>(
  dataForOnePage =>
  {
    // merge dataForOnePage into allData
  });

然后我们将三个块链接在一起以创建网格:

downloadData.LinkTo(parseHtml);
parseHtml.LinkTo(merge);

接下来,我们开始将数据泵入网格:

foreach (var pageid in the8000urls)
  downloadData.Post(pageid);

最后,我们等待网格中的每一步完成(这也会干净地传播任何错误):

downloadData.Complete();
await downloadData.Completion;
parseHtml.Complete();
await parseHtml.Completion;
merge.Complete();
await merge.Completion;

TPL Dataflow的优点在于您可以轻松控制每个部分的并行。目前,我已将下载和解析块设置为Unbounded,但您可能希望限制它们。合并块使用默认的最大并行度1,因此合并时不需要锁定。

答案 2 :(得分:1)

我建议您阅读reasonably-complete introduction to async/await

首先,让所有东西都异步,从较低级别的东西开始:

public static async Task<DataSet> ScrapeDataAsync(string pageid)
{
  CookieAwareWebClient webClient = ...;
  var dsPageData = new DataSet();

  // DOWNLOAD HTML FOR THE REO PAGE AND LOAD IT INTO AN HTMLDOCUMENT
  string url = @"https://domain.com?&id=" + pageid + @"restofurl";
  string html = await webClient.DownloadStringTaskAsync(url).ConfigureAwait(false);
  var doc = new HtmlDocument();
  doc.LoadHtml(html);

  // A BUNCH OF PARSING WITH HTMLAGILITY AND STORING IN dsPageData 
  return dsPageData;
}

然后您可以按如下方式使用它(使用async和LINQ):

DataSet alldata;
var tasks = the8000urls.Select(async url =>
{
  var dataForOnePage = await ScrapeDataAsync(url);

  //merge each table in dataForOnePage into allData

});
await Task.WhenAll(tasks);
PushAllDataToSql(alldata);

并使用我的AsyncEx库中的AsyncContext,因为这是a console app

class Program
{
  static int Main(string[] args)
  {
    try
    {
      return AsyncContext.Run(() => MainAsync(args));
    }
    catch (Exception ex)
    {
      Console.Error.WriteLine(ex);
      return -1;
    }
  }

  static async Task<int> MainAsync(string[] args)
  {
    ...
  }
}

就是这样。无需锁定或延续或任何此类。

答案 3 :(得分:-1)

我相信你这里不需要asyncawait。他们可以帮助您在需要将工作转移到非GUI线程的桌面应用程序中。在我看来,最好在你的情况下使用Parallel.ForEach方法。像这样:

    DataSet alldata;
    var bag = new ConcurrentBag<DataSet>();

    Parallel.ForEach(the8000urls, url =>
    {
        // ScrapeData downloads the html from the url with WebClient.DownloadString 
        // and scrapes the data into several datatables which it returns as a dataset. 
        DataSet dataForOnePage = ScrapeData(url);
        // Add data for one page to temp bag
        bag.Add(dataForOnePage);
    });

    //merge each table in dataForOnePage into allData from bag

    PushAllDataToSql(alldata);