我一直在考虑让我的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
});
这会有用还是我弄错了?
答案 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个请求/秒。