网页爬虫中的多线程模式

前天跟师兄讨论问题,提到多线程,这些天做简历,也在回顾项目,忽然想到曾经写过网络爬虫中所用到的多线程,当时就顾写了,没有好好总结,只记得细节很多,学到的东西不少,今天就爬虫中涉及到的多线程设计模式做个小整理,重点加深读写锁模式的理解。内容如下:

===问题细节说明

===网页抓取:生产者消费者模式(多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只有在同步块中才有意义;

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REaDME.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值