关键技术之单机爬虫的实现(2)---多线程?
可能是上次的写作风格自己觉得也比较别扭。这样就正儿八经的写写这篇文章。总之,一句话。什么是好文章,难让有一定基础的人看懂看完学到东西的文章就是好文章。我希望能达到这种效果。
上篇文章其实做的一个很简单的爬虫原型。采用的就是在单线程阻塞形式(通过函数之间调用)的运行爬虫爬行的过程。其中有些网友在评论中提到更好的方法。这个问题其实是很多系统为提高效率必须得考虑的。我一直觉得,其实项目压根不需要做多。踏踏实实的做好一个东西,做深了就足够了。尤其是还在大学奋斗的同学们。大学四年,你只要尽能力做好一个东西还怕找不到工作吗?
网络爬虫最重要的一点是什么?性能,影响性能的原因很多。网络爬虫的通信方式可以说是最为重要的。比较上篇文章中采用的方式,性能肯定很低。每一步基础操作都必须等待前面的一些操作。引入今天的问题。
阻塞和非阻塞
函数在完成其指定的任务以前不允许程序调用另一个函数。例如,程序执行一个读数据的函数调用时,在此函数完成读操作以前将不会执行下一程序语句。当服务器运行到accept语句时,而没有客户连接服务请求到来,服务器就会停止在accept语句上等待连接服务请求的到来。这种情况称为阻塞(blocking)。而非阻塞操作则可以立即完成。比如,如果你希望服务器仅仅注意检查是否有客户在等待连接,有就接受连接,否则就继续做其他事情,则可以通过将Socket设置为非阻塞方式来实现。非阻塞Socket在没有客户在等待时就使accept调用立即返回。通过设置Socket为非阻塞方式,可以实现"轮询"若干Socket。但是这种"轮询"会使CPU处于忙等待方式,从而降低性能,浪费系统资源。
三种方案
第一种是单线程阻塞,在这种模式下,程序仅有一个线程在运行,即DNS解析,建立连接,写入请求,读取结果都是顺序执行的,不能同时进行。这是最简单也最容易实现的一种,同时它的效率问题也显而易见:由于是阻塞方式读取,DNS解析,建立连接,写入请求,读取结果,这些步骤上都会产生时间的延迟,只有在前一个模块完成后,下一个模块才能进行,从而无法有效的利用服务器的全部资源,效率比较低。
第二种是多线程阻塞。不少爬虫都采用这种设计方式,建立多个阻塞的线程,分别请求不同的URL。或者对于不同的模块分别用不同的线程来实现,该模式的优点显而易见,相对于第一种方法,它可以实现了并行的操作,可以更有效的利用机器的资源,特别是网络资源,因为无数线程在同时工作,所以网络会比较充分的利用,但缺点也不容忽视,由于CPU始终处于"轮询"状态,会对机器CPU资源的消耗也是比较大,而且很多操作系统对线程数有一定限制,特别是在用户级多线程间的频繁切换对于性能的影响较大。当然,可以使用线程池来缓解CPU的压力。
第三种是单线程非阻塞。使用非阻塞I/O的程序一般是单线程的(有时可能将使用非阻塞I/O的程序段放到一个单独的线程里,而主线程负责处理用户的输入),使用单线程非阻塞不会像多线程那样由于线程间频繁切换造成的性能消耗。不过它不太适当做保持连接的通信。
考虑各方面原因,这里采用多线程阻塞形式。
spider相关代码:
public class Spider
{
public SpiderWork[] dts = new SpiderWork[Settings.threadcount];//线程池
private List<int> threads = new List<int>();
public void Init() // 初始化url队列
{
UrlQueueManager.Init();
}
// 从下载队列中获得一定数量的url
public void getUrls(LinkedList<string> urls, int count)
{
for (int i = 0; i < count; i++)
{
if(UrlQueueManager.Count>0)
{
urls.AddLast(UrlQueueManager.Dequeue().ToString());
}
else
break;
}
}
public void addUrl(string url) // 将URL加到下载队列中
{
if (UrlFilter.isOK(url))//过滤url
{
UrlInfo urls = new UrlInfo();
urls.Url = url;
UrlQueueManager.Enqueue(urls);
UrlFilter.addUrl(url);
}
}
public void threadWait(int threadID)//让某线程休眠
{
if (!threads.Contains(threadID))
threads.Add(threadID);
}
public bool isFinished() // 如果所有的线程都处于休眠状态,下载工作结束
{
if (dts.Length == threads.Count)
return true;
else
return false;
}
public void startThreads() // 创建并运行用于下载网络资源的线程
{
for (int i = 0; i < dts.Length; i++)
{
dts[i] = new SpiderWork();
dts[i].threadID = i;
dts[i].start();
}
}
public void stopThreads() // 停止所有的下载线程
{
for (int i = 0; i < dts.Length; i++)
{
dts[i].stop = true;
}
}
}
可能从上面看出来,通过线程数组,我们可以造成一个最简单的线程池。当然目前这些都是些基本功能。我们可能考虑设置当先线程busy变量,这样就能充分利用空闲线程来分配任务。待续...