网页爬虫中的多线程模式
2013-09-14 10:00
232 查看
前天跟师兄讨论问题,提到多线程,这些天做简历,也在回顾项目,忽然想到曾经写过网络爬虫中所用到的多线程,当时就顾写了,没有好好总结,只记得细节很多,学到的东西不少,今天就爬虫中涉及到的多线程设计模式做个小整理,重点加深读写锁模式的理解。内容如下:
===问题细节说明
===网页抓取:生产者消费者模式(多v多)
===URL去重:读写锁模式
===网页写入文件:生产者消费者模式(多v一)
===关于多线程的几点注意
=========================================
问题细节说明
1)简述一下这里涉及到的三个过程:a)爬虫从待爬取URL队列中取得URL进行抓取,抓取来的网页进行解析提取新的链接加入到待爬取URL队列;b)爬取每个网页之前,程序会到已爬取URL表中查询该URL是否已经爬取过;c)网页爬取完,网页解析的内容要写入文件
上述三个过程都是在多线程的环境下进行处理,涉及到的共享资源有:URL任务队列、已爬取的URL表,存储网页内容的文件。这些共享资源的读与写都需要处理好线程的同步互斥,以保证线程安全。
2)注:爬虫中多线程的管理实际是需要维护一个线程池;URL去重也是使用MD5结合布隆过滤器进行实现的;上述所述三个过程在爬虫中并不是独立的,而是互相结合工作的,但是为了提取模式特点,我们将其分拆开,在讲述一个模式过程中如果涉及到其他模式,便略去不谈。所以,这里仅仅提取爬虫中的多线程设计模式,举例重在了解模式工作原理,不涉及爬虫具体实现的过多细节。
=========================================
网页抓取:生产者消费者模式(多v多)
1) 生产者消费者模式
学过操作系统应该对这个概念很熟悉,简单来说就是“你生产我消费”;该模式分为三个部分:生产者,消费者和产品队列(爬虫中该产品就是URL任务队列,故以下称任务队列)。
该模式可灵活应用,生产者和消费者的比例可以是:多v多,多v一,一v多;
该模式解决多线程同步问题的思想是:对任务队列加锁,队列的出队和入队操作原子性;
该模式应用到多线程爬虫:抓取线程主动去任务队列找活干,如果没活就等待,有活了就通知那些等待的抓取线程。
2) 简单示例代码
在爬虫中,生产者是网页的抓取线程,消费者也是网页的抓取线程,所以这里只要两个类就可以了,一个网页抓取线程类WorkThread,一个任务队列类TaskQueue;简单示例如下,在TaskQueue类中,使用Synchronized关键字使当前线程取得对象TaskQueue的锁,并用wait和notifyAll函数进行线程间通信。
=========================================
URL去重:读写锁模式
1) 读写锁模式
读写锁模式简单来说就是“读写互斥,写写互斥,读读不互斥”,即大家都可以来看,但是看的时候不能写,写的时候不能看,且某一时刻只能有一个人写。
该模式分为四个部分:写入者,读者,数据对象(可读可写的类),读写锁(提供读写锁定的类);
该模式应用到爬虫中:写入者和读者就都是网页抓取线程WorkThread了;数据对象便是存储已爬取过的URL列表,一般使用Hash或Bloom Filter,后面统一称URL散列表,记为UrlLib;读写锁便是控制读写互斥机制的类,记为ReadWriteLock。
2) 简单示例代码
关于本示例代码的解读请见第三条的读写锁模式分析说明,看下面的代码是如何完成“读读不互斥,读写以及写写互斥的”,个人觉得该模式还是很有智慧的,巧妙地利用了“物理锁”产生了“逻辑锁”的效果。
3) 读写锁模式分析说明
a) readingReaders字段:
readingReaders在readLock方法后面递增,而在readUnlock方法前面递减,保证了readingReaders字段能够表示正在读取的线程数量(通过了readLock后,还没有通过readUnlock的线程数量),被读取的对象数据UrlLib本身并没有加锁,线程获取释放的锁是ReadWriteLock实例的锁,所以可以同时有多个线程在读取UrlLib,即一个线程经过readlock后就释放掉了锁,其他读者线程还可以继续获取,可以用下面示意图表示
b) waitingWriters 和 preferWriter字段
waitingWriters表示调用writeLock时,就进入wait状态的线程,注意这一点很重要,一定要在线程wait之前将该字段加1,之后无论发生什么情况(该方法被打断或正常执行),该字段一定要减1,这也是try-finally的作用;
preferWriter保证在下面情景:当前有线程在读取数据(注意:读取的数据对象是没有锁的,读写锁的获取与释放与当前正读取数据的线程无关),然后先来线程1要写,一看有人在读,进入等待,又来一线程2要读,一看有preferWriter优先并且有写线程在等,所以它也进入等待,直到写完等待的条件才不成立,才有机会读;理论上我们就要保证当前所有读取数据的线程执行完毕后,先让线程1写,然后让线程2读,所以在readUnlock时设定preferWriter字段为true保证了上面我们说的情景。
如果把这两个字段去掉的话,那么程序就变成了无论写者线程何时来,只要它在等待的时候后面有读者线程来,那么它就一直得不到机会进行写入(保证不了先来后到的原则了),程序可能就会发生写者线程饿死情况,理解了上面的分析这一点就很容易理解了。
c) 物理锁与逻辑锁
该模式很好地利用了物理锁产生了一个逻辑锁,从而实现读读不互斥,但读写互斥的效果;物理锁便是java对象实例存在的一把锁,该模式中就是ReadWriteLock实例的锁,逻辑锁便是“读锁”与“写锁”;物理锁是实实在在存在的,整个过程利用的实例锁也就这一把,但是却实现了逻辑上的读锁与写锁的效果,读锁的利用也使得程序的并发性更高,使得多个线程可以同时读取数据对象。
d) 读写锁的应用场景
读写锁一般应用在读取查询较为频繁,写入不频繁的时候,其实爬虫中这部分完全也可以做成读写、读读、写写都互斥的形式,小规模情况下没啥影响,这样写就简单了很多,只要给UrlLib实例进行加锁就行了,不需要ReadWriteLock来实现逻辑锁了。
=========================================
网页写入文件:生产者消费者模式(多v一)
这一部分与第一部分类似,就不详述了,这里只是将其提出来,读写锁的彻底明白让我费了好大劲,这部分就简述了。
这里的生产者便是网页抓取线程WorkThread了,消费者是将网页内容写入文件的OutputThread,而产品队列便是存储网页对象的PageQueue类了,这里将这部分列出就是为了说明生产者消费者模式的多样化、实用性以及队列的重要性。
=========================================
关于多线程的几点注意:
1) 获得锁:synchronized关键字获得实例锁;每个实例只有一把锁,当用synchronized时一定要明白“要保护什么,获得谁的锁?”,synchronized用法常见两种:
a) synchronized方法,执行该方法的当前线程获得当前实例对象的锁
b) synchronized(object),该代码块开始当前线程获得object对象的锁
2) 释放锁:a)当synchronized代码块执行完毕后,释放锁;b)当执行wait时,该线程进入到当前对象的等待池,释放锁
3) wait方法两点注意:
a) wait()方法一般或者必须放在一个while循环中,因为在多线程环境中,共享对象的状态随时可能改变。当一个线程在对象等待池中从wait状态被唤醒后,并不一定立即恢复运行,必须要等到这个线程获得了锁及CPU才能继续运行,有可能在被唤醒后而获得锁之前时,对象的状态已经发生了变化或者锁已经被其他线程获取了。
b) wait方法一定要在synchronized的同步块代码当中才有意义,因为wait方法是进入对象等待池并释放锁,也就是说在调用wait方法时,当前的线程一定要获得对象的锁才行。
4) sleep与wait区别:Sleep是Thread类方法,让线程停转,一段时间恢复;wait是Object类方法,用来线程间通信的,它使得当前拥有该对象的锁的进程进入等待状态并释放锁;另外,wait只有在同步块中才有意义;
===问题细节说明
===网页抓取:生产者消费者模式(多v多)
===URL去重:读写锁模式
===网页写入文件:生产者消费者模式(多v一)
===关于多线程的几点注意
=========================================
问题细节说明
1)简述一下这里涉及到的三个过程:a)爬虫从待爬取URL队列中取得URL进行抓取,抓取来的网页进行解析提取新的链接加入到待爬取URL队列;b)爬取每个网页之前,程序会到已爬取URL表中查询该URL是否已经爬取过;c)网页爬取完,网页解析的内容要写入文件
上述三个过程都是在多线程的环境下进行处理,涉及到的共享资源有:URL任务队列、已爬取的URL表,存储网页内容的文件。这些共享资源的读与写都需要处理好线程的同步互斥,以保证线程安全。
2)注:爬虫中多线程的管理实际是需要维护一个线程池;URL去重也是使用MD5结合布隆过滤器进行实现的;上述所述三个过程在爬虫中并不是独立的,而是互相结合工作的,但是为了提取模式特点,我们将其分拆开,在讲述一个模式过程中如果涉及到其他模式,便略去不谈。所以,这里仅仅提取爬虫中的多线程设计模式,举例重在了解模式工作原理,不涉及爬虫具体实现的过多细节。
=========================================
网页抓取:生产者消费者模式(多v多)
1) 生产者消费者模式
学过操作系统应该对这个概念很熟悉,简单来说就是“你生产我消费”;该模式分为三个部分:生产者,消费者和产品队列(爬虫中该产品就是URL任务队列,故以下称任务队列)。
该模式可灵活应用,生产者和消费者的比例可以是:多v多,多v一,一v多;
该模式解决多线程同步问题的思想是:对任务队列加锁,队列的出队和入队操作原子性;
该模式应用到多线程爬虫:抓取线程主动去任务队列找活干,如果没活就等待,有活了就通知那些等待的抓取线程。
2) 简单示例代码
在爬虫中,生产者是网页的抓取线程,消费者也是网页的抓取线程,所以这里只要两个类就可以了,一个网页抓取线程类WorkThread,一个任务队列类TaskQueue;简单示例如下,在TaskQueue类中,使用Synchronized关键字使当前线程取得对象TaskQueue的锁,并用wait和notifyAll函数进行线程间通信。
/* 任务队列类 */ public class TaskQueue { private Queue<url> taskbuffer; private int taskcount; private int maxN; public TaskQueue(int max) { maxN = max; taskbuffer = new Queue<url>(maxN); taskcount = 0; } // 生产者调用的任务入队操作 public synchronized void addTask(List<url> urlList) { while(maxN <= taskcount)// 如果队列够大,while语句块可以略 { wait(); // 进入TaskQueue对象等待池,释放锁 } while(!urlList.isEmpty()) { taskbuffer.add(str); taskcount++; } notifyAll(); // 唤醒等待池线程 } // 消费者调动的任务出队操作 public synchronized url getTask() { while(taskcount <= 0) { wait(); } url str = taskbuffer.pop(); taskcount--; return str; } } /* 扮演生产者和消费者的网页抓取线程 */ public class WorkThread extends Thread { private TaskQueue taskQueue; private List<url> urlList; //下面有完善。。。读写锁 public WorkThread(TaskQueue tq) { taskQueue = tq; urlList = new List<url>(); } public List<url> crawl(url str) { /* crawl and parse the page str * return the parse link list of page str */ } public void run() { try { while (true) { Thread.sleep(1000); url str = taskQueue.getTask(); // 扮演消费者 urlList = crawl(str); // 略去url去重,详见读写锁模式 tastQueue.addTask(urlList); // 扮演生成者 } } catch (InterruptedException e) { } } }
=========================================
URL去重:读写锁模式
1) 读写锁模式
读写锁模式简单来说就是“读写互斥,写写互斥,读读不互斥”,即大家都可以来看,但是看的时候不能写,写的时候不能看,且某一时刻只能有一个人写。
该模式分为四个部分:写入者,读者,数据对象(可读可写的类),读写锁(提供读写锁定的类);
该模式应用到爬虫中:写入者和读者就都是网页抓取线程WorkThread了;数据对象便是存储已爬取过的URL列表,一般使用Hash或Bloom Filter,后面统一称URL散列表,记为UrlLib;读写锁便是控制读写互斥机制的类,记为ReadWriteLock。
2) 简单示例代码
关于本示例代码的解读请见第三条的读写锁模式分析说明,看下面的代码是如何完成“读读不互斥,读写以及写写互斥的”,个人觉得该模式还是很有智慧的,巧妙地利用了“物理锁”产生了“逻辑锁”的效果。
/* 读写锁 */ public class ReadWriteLock { private int readingReaders = 0; // (A)正在执行读取的线程数量 private int waitingWriters = 0; // (B)正在等待写入的线程数量 private int writingWriters = 0; // (C)正在执行写入的线程数量 private boolean preferWriter = true; // 写入优先时,值为true // 获取逻辑读锁 public synchronized void readLock() throws InterruptedException { while(writingWriters > 0 || (preferWriter && waitingWriters > 0)) { wait(); } readingReaders++; // (A)正在执行读取的线程数加1 } // 逻辑解读锁 public synchronized void readUnlock() { readingReaders--; // (A)正在执行读取的线程数减1 preferWriter = true; // 保证先来的写者能比后来的读者优先级高 notifyAll(); } // 获取逻辑写锁 public synchronized void writeLock() throws InterruptedException { waitingWriters++; // (B)正在等待写入的线程数加1 try{ while(readingReaders > 0 || writingWriters > 0) { wait(); } }finally{ waitingWriters--; // (B)正在等待写入的线程数减1 } writingWriters++; // (C)正在执行写入的线程数加1 } // 逻辑解写锁 public synchronized void writeUnlock() { writingWriters--; // (C)正在执行写入的线程数减1 preferWriter = false; notifyAll(); } } /* Url散列表类 * 注:简单起见,下面的代码类似伪代码,不一定符合语法,例如hashtable */ public class UrlLib { private Hashtable<url> hash; // 存储已经爬取的Url private ReadWriteLock lock = new ReadWriteLock(); public UrlLib() { hash = new Hashtable<url>(); } // 读取操作:查询是否含有url public boolean query(url str) throws Interrupted Exception { lock.readLock(); try{ return hash.contains(str) ? true : false; }finally{ lock.readUnlock(); } // 使用finally保证return后还能执行解锁操作 } // 写入操作 public void add(url str) throws Interrupted Exception { lock.writeLock(); try{ hash.insert(str); }finally{ lock.writeUnlock(); } } } /* 扮演读者和写者的网页抓取线程 */ public class WorkThread extends Thread { private TaskQueue taskQueue; private UrlLib urlHash; public WorkThread(UrlLib ul) { urlHash = ul; } public void run() { try { while (true) { Thread.sleep(1000); url str = taskQueue.getTask(); // 取得URL if(!urlHash.query(str)) // 扮演读者读取操作 { this.crawl(str); // 如果UrlLib中没有则爬取 urlHash.add(str); // 扮演写者,爬取完将url写入散列表 } } } catch (InterruptedException e) { // ReadWriteLock与UrlLib中的异常统一在这里捕获 } } }
3) 读写锁模式分析说明
a) readingReaders字段:
readingReaders在readLock方法后面递增,而在readUnlock方法前面递减,保证了readingReaders字段能够表示正在读取的线程数量(通过了readLock后,还没有通过readUnlock的线程数量),被读取的对象数据UrlLib本身并没有加锁,线程获取释放的锁是ReadWriteLock实例的锁,所以可以同时有多个线程在读取UrlLib,即一个线程经过readlock后就释放掉了锁,其他读者线程还可以继续获取,可以用下面示意图表示
b) waitingWriters 和 preferWriter字段
waitingWriters表示调用writeLock时,就进入wait状态的线程,注意这一点很重要,一定要在线程wait之前将该字段加1,之后无论发生什么情况(该方法被打断或正常执行),该字段一定要减1,这也是try-finally的作用;
preferWriter保证在下面情景:当前有线程在读取数据(注意:读取的数据对象是没有锁的,读写锁的获取与释放与当前正读取数据的线程无关),然后先来线程1要写,一看有人在读,进入等待,又来一线程2要读,一看有preferWriter优先并且有写线程在等,所以它也进入等待,直到写完等待的条件才不成立,才有机会读;理论上我们就要保证当前所有读取数据的线程执行完毕后,先让线程1写,然后让线程2读,所以在readUnlock时设定preferWriter字段为true保证了上面我们说的情景。
如果把这两个字段去掉的话,那么程序就变成了无论写者线程何时来,只要它在等待的时候后面有读者线程来,那么它就一直得不到机会进行写入(保证不了先来后到的原则了),程序可能就会发生写者线程饿死情况,理解了上面的分析这一点就很容易理解了。
c) 物理锁与逻辑锁
该模式很好地利用了物理锁产生了一个逻辑锁,从而实现读读不互斥,但读写互斥的效果;物理锁便是java对象实例存在的一把锁,该模式中就是ReadWriteLock实例的锁,逻辑锁便是“读锁”与“写锁”;物理锁是实实在在存在的,整个过程利用的实例锁也就这一把,但是却实现了逻辑上的读锁与写锁的效果,读锁的利用也使得程序的并发性更高,使得多个线程可以同时读取数据对象。
d) 读写锁的应用场景
读写锁一般应用在读取查询较为频繁,写入不频繁的时候,其实爬虫中这部分完全也可以做成读写、读读、写写都互斥的形式,小规模情况下没啥影响,这样写就简单了很多,只要给UrlLib实例进行加锁就行了,不需要ReadWriteLock来实现逻辑锁了。
=========================================
网页写入文件:生产者消费者模式(多v一)
这一部分与第一部分类似,就不详述了,这里只是将其提出来,读写锁的彻底明白让我费了好大劲,这部分就简述了。
这里的生产者便是网页抓取线程WorkThread了,消费者是将网页内容写入文件的OutputThread,而产品队列便是存储网页对象的PageQueue类了,这里将这部分列出就是为了说明生产者消费者模式的多样化、实用性以及队列的重要性。
=========================================
关于多线程的几点注意:
1) 获得锁:synchronized关键字获得实例锁;每个实例只有一把锁,当用synchronized时一定要明白“要保护什么,获得谁的锁?”,synchronized用法常见两种:
a) synchronized方法,执行该方法的当前线程获得当前实例对象的锁
b) synchronized(object),该代码块开始当前线程获得object对象的锁
2) 释放锁:a)当synchronized代码块执行完毕后,释放锁;b)当执行wait时,该线程进入到当前对象的等待池,释放锁
3) wait方法两点注意:
a) wait()方法一般或者必须放在一个while循环中,因为在多线程环境中,共享对象的状态随时可能改变。当一个线程在对象等待池中从wait状态被唤醒后,并不一定立即恢复运行,必须要等到这个线程获得了锁及CPU才能继续运行,有可能在被唤醒后而获得锁之前时,对象的状态已经发生了变化或者锁已经被其他线程获取了。
b) wait方法一定要在synchronized的同步块代码当中才有意义,因为wait方法是进入对象等待池并释放锁,也就是说在调用wait方法时,当前的线程一定要获得对象的锁才行。
4) sleep与wait区别:Sleep是Thread类方法,让线程停转,一段时间恢复;wait是Object类方法,用来线程间通信的,它使得当前拥有该对象的锁的进程进入等待状态并释放锁;另外,wait只有在同步块中才有意义;
相关文章推荐
- 基python实现多线程网页爬虫
- 多线程网页爬虫 python 实现
- Python+正则表达式编写多线程百度贴吧网页爬虫
- 多线程获取豆瓣网页的网络爬虫(Python实现)
- 用python写的多线程网页爬虫
- 网络爬虫:使用多线程爬取网页链接
- Python之多线程爬虫抓取网页图片的示例代码
- Python爬虫之路——简单网页抓图升级版(增加多线程支持)
- 网络爬虫:使用多线程爬取网页链接
- 多线程网页爬虫 python 实现(二)
- 网络爬虫:使用多线程爬取网页链接
- python多线程爬虫抓取网页
- 基python实现多线程网页爬虫
- php多线程采集网页数据-php采集网页-php爬虫视频教程8
- C#多线程使用webbrowser实现采集动态网页的爬虫机器人
- Thread-Per-message Pattern--JAVA多线程编程模式(7)
- java多线程之单例模式和其DCL问题
- 多线程环境下单例模式的两种实现方法
- 【python爬虫】利用selenium和Chrome浏览器进行自动化网页搜索与浏览
- 多线程下的单例模式