使用未知大小的队列的Web爬网程序的生产者/使用者

时间:2011-12-12 15:31:37

标签: c# multithreading queue web-crawler producer-consumer

我需要抓取父网页及其子网页,我遵循http://www.albahari.com/threading/part4.aspx#%5FWait%5Fand%5FPulse中的生产者/消费者概念。另外,我使用5个线程将​​链接排队并出列。

如果队列长度未知,那么在所有线程完成所有线程处理后,如何结束/加入所有线程的任何建议?

以下是关于我如何编码的想法。

static void Main(string[] args)
{
    //enqueue parent links here
    ...
    //then start crawling via threading
    ...
}

public void Crawl()
{
   //dequeue
   //get child links
   //enqueue child links
}

4 个答案:

答案 0 :(得分:3)

如果所有线程都空闲(即在队列中等待)并且队列为空,那么就完成了。

处理这种情况的一种简单方法是让线程在尝试访问队列时使用超时。像BlockingCollection.TryTake这样的东西。每当TryTake超时时,线程就会更新一个字段,说明它已经闲置了多长时间:

while (!queue.TryTake(out item, 5000, token))
{
    if (token.IsCancellationRequested)
        break;
    // here, update idle counter
}

然后,您可以拥有一个每15秒执行一次的计时器来检查所有线程的空闲计数器。如果所有线程都空闲了一段时间(可能是一分钟),那么计时器可以设置取消令牌。这将杀死所有线程。您的主程序也可以监控取消令牌。

顺便提一句,您可以在没有BlockingCollection的情况下执行此操作并取消。您只需要创建自己的取消信号机制,如果您在队列上使用锁,则可以用Monitor.TryEnter等替换锁定语法。

还有其他几种方法可以解决这个问题,尽管它们需要对您的程序进行一些重大改组。

答案 1 :(得分:1)

您可以在最后将虚拟令牌排入队列,并在遇到此令牌时让线程退出。像:

public void Crawl()
{
   int report = 0;
   while(true)
   {
       if(!(queue.Count == 0))      
       {   
          if(report > 0) Interlocked.Decrement(ref report);
          //dequeue     
          if(token == "TERMINATION")
             return;
          else
             //enqueue child links
       }
       else
       {              
          if(report == num_threads) // all threads have signaled empty queue
             queue.Enqueue("TERMINATION");
          else
             Interlocked.Increment(ref report); // this thread has found the queue empty
       }
    }
}

当然,我省略了enqueue/dequeue操作的锁定。

答案 2 :(得分:0)

线程可以发出信号,表示已经结束了他们的工作,例如举起一个事件,或者调用一个代表。

static void Main(string[] args)
{
//enqueue parent links here
...
//then start crawling via threading
...
}

public void X()
{
    //block the threads until all of them are here
}

public void Crawl(Action x)
{
    //dequeue
    //get child links
    //enqueue child links
    //call x()
}

答案 3 :(得分:0)

如果您愿意使用Task Parallel Library,则无需手动处理生产者 - 消费者资料。使用AttachToParent选项创建任务时,子任务将以父任务链接的方式在子任务完成之前不会完成。

class Program
{
    static void Main(string[] args)
    {
        var task = CrawlAsync("http://stackoverflow.com");
        task.Wait();
    }

    static Task CrawlAsync(string url)
    {
        return Task.Factory.StartNew(
            () =>
            {
                string[] children = ExtractChildren(url);
                foreach (string child in children)
                {
                    CrawlAsync(child);
                }
                ProcessUrl(url);
            }, TaskCreationOptions.AttachedToParent);
    }

    static string[] ExtractChildren(string root)
    {
      // Return all child urls here.
    }

    static void ProcessUrl(string url)
    {
      // Process the url here.
    }
}

您可以使用Parallel.ForEach删除一些显式任务创建逻辑。