2012-02-13 47 views
3

嘗試使用Async CTP編寫HTML搜尋器我已經停滯不前,不知道如何編寫無遞歸方法來完成此任務。使用異步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執行第一個(也是唯一的迭代)的時候希望我只有只有1項。我可以想到的最簡單的方法,使ForEach遞歸,但我不能(我不認爲)這樣做,因爲我會很快用完堆棧空間。

任何人都可以請指導我如何重構此代碼,創建我將描述爲遞歸延續,直到達到MaxStackSize或系統內存不足時遞增項目?

+0

+1。控制遞歸的人控制着宇宙! – toddmo 2013-12-14 03:31:12

回答

10

我認爲使用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; 
} 
+0

哇!謝謝你的精彩解釋。你能確認一件事嗎?如果我們將MaxDegreeOfParallelism設置爲大於1的數字,這是否意味着我需要將集合類型更改爲類似於ConcurrentStack的東西,因爲它是線程安全的? – 2012-02-15 08:59:58

+0

你的意思是'storeBlock'中的集合?你在哪裏設置MaxDegreeOfParallelism?如果你將'storeBlock'的'MDOP'設置爲> 1,那麼你需要在那裏使用一些線程安全的集合(或使用鎖)。但是,如果將其他塊的「MDOP」設置爲> 1,則不會影響「storeBlock」的並行性,因此您不需要考慮線程安全性。 – svick 2012-02-15 11:54:36

+0

這個男孩會讓我升級到2012! +1 – toddmo 2013-12-14 03:29:14