2012-02-13 6 views
3

Tenter d'écrire un moteur de balayage HTML à l'aide du CTP Async J'ai été bloqué quant à la façon d'écrire une méthode sans récursion pour accomplir ceci.Télécharger des pages HTML simultanément à l'aide du CTP Async

C'est le code que j'ai jusqu'à présent.

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

Mais, évidemment, cela ne fonctionne pas comme je l'espère, car au moment où Parallel.ForEach exécute la première (et seule itération) Je ne seulement 1 point. L'approche la plus simple que je puisse imaginer pour rendre le ForEach récursif mais je ne peux pas (je ne pense pas) le faire car je serais rapidement à court d'espace de pile.

Quelqu'un peut-il me guider sur la façon dont je peux restructurer ce code, pour créer ce que je décrirais comme une continuation récursive qui ajoute des éléments jusqu'à ce que le MaxStackSize soit atteint ou le système manque de mémoire?

+0

+1. Celui qui contrôle la récursivité, contrôle l'univers! – toddmo

Répondre

10

Je pense que la meilleure façon de faire quelque chose comme ça en utilisant C# 5/.Net 4.5 est d'utiliser TPL Dataflow. Il y a même a walkthrough on how to implement web crawler using it.

Fondamentalement, vous créez un « bloc » qui prend en charge le téléchargement d'une URL et d'obtenir le lien de celui-ci:

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

Vous pouvez définir MaxDegreeOfParallelism à une valeur que vous voulez. Il indique au plus combien d'URL peuvent être téléchargées simultanément. Si vous ne voulez pas le limiter du tout, vous pouvez le définir sur DataflowBlockOptions.Unbounded.

Ensuite, vous créez un bloc qui traite en quelque sorte tous les liens téléchargés, comme les stocker tous dans une liste. Il peut également décider quand pour annuler le téléchargement:

var links = new List<LinkItem>(); 

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

Puisque nous ne l'avons pas mis MaxDegreeOfParallelism, la valeur par défaut 1. Cela signifie en utilisant la collecte qui ne sont pas thread-safe devrait être correct ici.

Nous créons un bloc de plus: il faudra un lien de linkFinderBlock, et passez-le tous les deux à storeBlock et retour à linkFinderBlock.

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

Le lambda dans son constructeur est une "fonction de clonage". Vous pouvez l'utiliser pour créer un clone de l'objet si vous le souhaitez, mais cela ne devrait pas être nécessaire ici, puisque nous ne modifions pas le LinkItem après la création.

Maintenant, nous pouvons relier les blocs ensemble:

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

Ensuite, nous pouvons commencer le traitement en donnant le premier élément à linkFinderBlock (ou broadcastBlock, si vous voulez envoyer aussi à storeBlock):

linkFinderBlock.Post(firstItem); 

Et enfin attendre jusqu'à ce que le traitement est terminé:

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

Wow! Merci pour la brillante explication. Pourriez-vous confirmer une chose? Si nous définissons MaxDegreeOfParallelism sur un nombre> 1 cela signifie-t-il que j'ai besoin de changer le type de collection pour quelque chose comme le ConcurrentStack pour qu'il soit thread safe? –

+0

Voulez-vous dire la collection dans 'storeBlock'? Et où définissez-vous 'MaxDegreeOfParallelism'? Si vous définissez 'MDOP' de' storeBlock' à> 1, alors oui, vous devrez utiliser une collection thread-safe (ou utiliser des verrous). Mais si vous définissez 'MDOP' d'un autre bloc à> 1, cela n'affecte pas le parallélisme de' storeBlock', donc vous n'avez pas besoin de considérer la sécurité des threads. – svick

+0

Garçon ça va me faire passer à 2012! +1 – toddmo