多线程网络刮刀?

时间:2013-04-20 00:32:25

标签: c# multithreading .net-4.0 web scraper

我一直在考虑让我的web scraper多线程,而不是像普通的线程(例如,线程scrape =新的线程(函数);)但是像线程池这样的东西可以有很多线程。

我的刮刀使用for循环来抓取页面。

for (int i = (int)pagesMin.Value; i <= (int)pagesMax.Value; i++)

那么我怎样才能将函数(包含循环)多线程与线程池一起多线程?我之前从未使用过线程池,我见过的例子让我很困惑或模糊不清。


我已将我的循环修改为:

int min = (int)pagesMin.Value;
int max = (int)pagesMax.Value;
ParallelOptions pOptions = new ParallelOptions();
pOptions.MaxDegreeOfParallelism = Properties.Settings.Default.Threads;
Parallel.For(min, max, pOptions, i =>{
    //Scraping
});

这会有用还是我弄错了?

5 个答案:

答案 0 :(得分:5)

使用池线程的问题是它们大部分时间都在等待来自Web站点的响应。使用Parallel.ForEach的问题在于它限制了您的并行性。

通过使用异步Web请求,我获得了最佳性能。我使用Semaphore来限制并发请求的数量,并且回调函数进行了抓取。

主线程创建Semaphore,如下所示:

Semaphore _requestsSemaphore = new Semaphore(20, 20);

20是通过反复试验得出的。事实证明,限制因素是DNS分辨率,平均而言,它需要大约50毫秒。至少,它确实在我的环境中。 20个并发请求是绝对最大值。 15可能更合理。

主线程实际上是循环的,如下所示:

while (true)
{
    _requestsSemaphore.WaitOne();
    string urlToCrawl = DequeueUrl();  // however you do that
    var request = (HttpWebRequest)WebRequest.Create(urlToCrawl);
    // set request properties as appropriate
    // and then do an asynchronous request
    request.BeginGetResponse(ResponseCallback, request);
}

将在池线程上调用的ResponseCallback方法执行处理,处理响应,然后释放信号量,以便可以进行另一个请求。

void ResponseCallback(IAsyncResult ir)
{
    try
    {
        var request = (HttpWebRequest)ir.AsyncState;
        // you'll want exception handling here
        using (var response = (HttpWebResponse)request.EndGetResponse(ir))
        {
            // process the response here.
        }
    }
    finally
    {
        // release the semaphore so that another request can be made
        _requestSemaphore.Release();
    }
}

正如我所说,限制因素是DNS解析。事实证明,DNS解析是在调用线程(在这种情况下是主线程)上完成的。有关详细信息,请参阅Is this really asynchronous?

这很容易实现并且运行良好。根据我的经验,这可能会获得超过20个并发请求,但这样做需要相当多的努力。我不得不做很多DNS缓存......好吧,这很难。

您可以使用Task和C#5.0(.NET 4.5)中的新异步内容来简化上述操作。不过,我对那些人怎么说还不够熟悉。

答案 1 :(得分:3)

最好使用TPL,即Parallel.ForEach使用带Partitioner的重载。它自动管理工作量。

FYI。你应该明白,更多的线程并不意味着更快。我建议你做一些测试来比较未参数化的Parallel.ForEach和用户定义的。

<强>更新

    public void ParallelScraper(int fromInclusive, int toExclusive,
                                Action<int> scrape, int desiredThreadsCount)
    {
        int chunkSize = (toExclusive - fromInclusive +
            desiredThreadsCount - 1) / desiredThreadsCount;
        ParallelOptions pOptions = new ParallelOptions
        {
            MaxDegreeOfParallelism = desiredThreadsCount
        };

        Parallel.ForEach(Partitioner.Create(fromInclusive, toExclusive, chunkSize),
            rng =>
            {
                for (int i = rng.Item1; i < rng.Item2; i++)
                    scrape(i);
            });
    }

注意在您的情况下,async可能会更好。

答案 2 :(得分:2)

如果你认为你的网络刮刀喜欢使用for循环,那么你可以看一下类似于foreach循环的 Parallel.ForEach();但是,它会迭代可枚举数据。 Parallel.ForEach 使用多个线程来调用循环体。

有关详细信息,请参阅Parallel loops

更新:

Parallel.For() Parallel.ForEach()非常相似,它取决于您用于或foreach循环的上下文。

答案 3 :(得分:0)

这是TPL Dataflow ActionBlock的完美场景。您可以轻松配置它以限制并发性。以下是文档中的一个示例:

var downloader = new ActionBlock<string>(async url =>
{
    byte [] imageData = await DownloadAsync(url);
    Process(imageData);
}, new DataflowBlockOptions { MaxDegreeOfParallelism = 5 }); 

downloader.Post("http://msdn.com/concurrency ");
downloader.Post("http://blogs.msdn.com/pfxteam");

您可以通过下载Introduction to TPL Dataflow来了解ActionBlock(包括引用的示例)。

答案 4 :(得分:0)

在我们的“Crawler-Lib Framework”测试期间,我发现并行,TPL或线程尝试无法获得您想要的吞吐量。您在本地计算机上停留在每秒300-500个请求上。如果要并行执行数千个请求,则必须执行它们的异步模式并并行处理结果。我们的Crawler-Lib Engine(一个支持工作流的请求处理器)在本地计算机上以大约10.000 - 20.000个请求/秒的速度执行此操作。如果你想拥有快速刮刀,请不要尝试使用TPL。而是使用异步模式(开始...结束...)并在一个线程中启动所有请求。

如果你的许多请求倾向于超时让我们说30秒后情况更糟。在这种情况下,基于TPL的解决方案将获得5的难看的糟糕吞吐量? 1?每秒请求数。异步模式每秒至少提供100-300个请求。 Crawler-Lib引擎可以很好地处理这个问题并获得最大可能的请求。假设您的TCP / IP大头钉配置为有60000个出站连接(65535是最大连接,因为每个连接都需要一个出站端口),那么您将获得60000个连接的吞吐量/ 30秒超时= 2000个请求/秒。