【问题标题】:Download HTML pages concurrently using the Async CTP使用 Async CTP 同时下载 HTML 页面
【发布时间】:2012-02-13 11:28:27
【问题描述】:

尝试使用 Async CTP 编写 HTML 爬虫,但我一直不知道如何编写无递归方法来完成此任务。

这是我目前的代码。

private readonly ConcurrentStack<LinkItem> _LinkStack;
private readonly Int32 _MaxStackSize;
private readonly WebClient client = new WebClient();

Func<string, string, Task<List<LinkItem>>> DownloadFromLink = async (BaseURL, uri) => 
{
    string html = await client.DownloadStringTaskAsync(uri);
    return LinkFinder.Find(html, BaseURL);
};

Action<LinkItem> DownloadAndPush = async (o) => 
{
    List<LinkItem> result = await DownloadFromLink(o.BaseURL, o.Href);
    if (this._LinkStack.Count() + result.Count <= this._MaxStackSize)
    {
        this._LinkStack.PushRange(result.ToArray());
        o.Processed = true;
    }  
};

Parallel.ForEach(this._LinkStack, (o) => 
{
    DownloadAndPush(o);
});

但显然这并不像我希望的那样有效,因为在Parallel.ForEach 执行第一次(也是唯一一次迭代)时,我只有一项。我能想到的使ForEach 递归的最简单方法,但我不能(我不认为)这样做,因为我会很快耗尽堆栈空间。

谁能指导我如何重构此代码,以创建我将描述为添加项目的递归延续,直到达到MaxStackSize 或系统内存不足?

【问题讨论】:

  • +1。谁控制了递归,谁就控制了宇宙!

标签: c# .net multithreading async-ctp async-await


【解决方案1】:

我认为使用 C# 5/.Net 4.5 执行此类操作的最佳方法是使用 TPL Dataflow。甚至还有a walkthrough on how to implement web crawler using it

基本上,您创建一个“块”来负责下载一个 URL 并从中获取链接:

var cts = new CancellationTokenSource();

Func<LinkItem, Task<IEnumerable<LinkItem>>> downloadFromLink =
    async link =>
            {
                // WebClient is not guaranteed to be thread-safe,
                // so we shouldn't use one shared instance
                var client = new WebClient();
                string html = await client.DownloadStringTaskAsync(link.Href);

                return LinkFinder.Find(html, link.BaseURL);
            };

var linkFinderBlock = new TransformManyBlock<LinkItem, LinkItem>(
    downloadFromLink,
    new ExecutionDataflowBlockOptions
    { MaxDegreeOfParallelism = 4, CancellationToken = cts.Token });

您可以将MaxDegreeOfParallelism 设置为您想要的任何值。它说最多可以同时下载多少个 URL。如果完全不想限制,可以设置为DataflowBlockOptions.Unbounded

然后您创建一个块以某种方式处理所有下载的链接,例如将它们全部存储在一个列表中。它还可以决定何时取消下载:

var links = new List<LinkItem>();

var storeBlock = new ActionBlock<LinkItem>(
    linkItem =>
    {
        links.Add(linkItem);
        if (links.Count == maxSize)
            cts.Cancel();
    });

由于我们没有设置MaxDegreeOfParallelism,它默认为1。这意味着在这里使用非线程安全的集合应该没问题。

我们再创建一个块:它将从linkFinderBlock 获取一个链接,并将其传递给storeBlock 并返回给linkFinderBlock

var broadcastBlock = new BroadcastBlock<LinkItem>(li => li);

其构造函数中的 lambda 是一个“克隆函数”。如果您愿意,您可以使用它来创建该项目的克隆,但这里应该没有必要,因为我们不会在创建后修改 LinkItem

现在我们可以将块连接在一起了:

linkFinderBlock.LinkTo(broadcastBlock);
broadcastBlock.LinkTo(storeBlock);
broadcastBlock.LinkTo(linkFinderBlock);

然后我们可以通过将第一项提供给linkFinderBlock(或broadcastBlock,如果您还想将其发送给storeBlock)来开始处理:

linkFinderBlock.Post(firstItem);

最后等到处理完成:

try
{
    linkFinderBlock.Completion.Wait();
}
catch (AggregateException ex)
{
    if (!(ex.InnerException is TaskCanceledException))
        throw;
}

【讨论】:

  • 哇!谢谢你精彩的解释。你能确认一件事吗?如果我们将 MaxDegreeOfParallelism 设置为一个大于 1 的数字,这是否意味着我需要将集合类型更改为 ConcurrentStack 之类的东西才能保证线程安全?
  • 你是指storeBlock中的收藏吗?你在哪里设置MaxDegreeOfParallelism?如果您将storeBlockMDOP 设置为> 1,那么,是的,您需要在那里使用一些线程安全的集合(或使用锁)。但是如果你把其他block的MDOP设置为>1,不会影响storeBlock的并行性,所以不需要考虑线程安全。
  • 天哪,这会让我升级到 2012 年! +1
猜你喜欢
  • 1970-01-01
  • 2011-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多