线程 start 之一个隐蔽的问题

我的爬虫执行流程如下图所示。


主线程启动,将 spider 初始化,调用 spider 的伪线程方法 start 方法,它会调用 linkParse , contentParse这个线程类的 start 方法和 SpiderManager 的伪线程 start 方法,而在SpiderManager 的 start 方法里面会调用两个 autoCheckPoint 线程的 start 和 一个 autoRestart 的 start 方法。上图中用绿色描底的类是线程类,它们各自做不同的事情。

linkParse 线程执行链接分析并将过滤过满足条件的 url 进入两个队列,用于下一次链接分析的 url 队列 linkParseQueue 和 网页内容解析 url 队列 contentparseQueue。contentParse 线程就是用来执行网页内容分析的。将链接分析和网页内容分析的工作完全独立开是经过深思熟虑之后的结果,这样能够起到很好的分布式扩展性,即可以用一台机器作为链接分析服务器,另一台机器作为网页内容分析服务器,另外维护一个网页分析 url 队列服务器(辅以缓存)。这种结构还有一种好处,就是比较好控制,两种工作互不干扰,独立工作。

另外三个线程,两个autoCheckpoint线程分别用来写 linkParseQueue 和 contentParseQueue 存档,将它们定时刷新到检查点文件中去,以防爬虫意外关闭,可以从上一次失败的地方继续爬取。autoRestart 线程用来使爬虫定时从最初的入口站点处开始爬取(只清空链接分析  url 队列,不清空内容分析 url 队列),这个设计主要是与具体的业务相关,因为要获得的内容具有时效性,它们在某些链接 url 上可能会定时更新,所以需要定时重新爬。autoRestart 通过调用 spider 的 restart 方法用来重新爬取。


介绍到这里,流程应该基本讲解清楚了。我遇到的问题就是,在调用  linkParse 的 start 方法时,发现在 start 里面的输出语句输出了两遍,而 start 方法里面做了判断,如果已经 start 被调用过,就不再调用。按理说,start 方法应该只能被调用一次,而且,程序的逻辑好像确实是这样的,即,只在程序最开始被主线程调用。

百思不得其解。后来,我想到那个 autoRestart 会不会有影响呢?因为它重启爬虫的逻辑是,如果爬虫已经在运行,就只清空 url 队列,并不会关闭线程后再开启线程,如果爬虫没有运行,则会调用  Spider 的 start 方法。所以,autoRestart 方法可能也会调用 Spider 的 start 方法,因而  linkParse 的 start 方法也会被间接调用。于是,做了一个实验,在 linkParse 的 stat 方法里面,我输出了线程的信息,果然,两个输出中,一个是主线程输出的,一个是 autoRestart 这个线程输出的。那么,在断定了问题在哪里之后,就开始分析为什么出现这种问题。把 Spider 的 start 方法和 reStart 方法贴过来:

public void start()
{
    if(!running)
    {
        spiderManager.start();
        urlLinkParse.start(config.getLinkParseThreads());
        urlContentParse.start(config.getContentParseThreads());
        running = true;
    }
}
    
    
public void reStart()
{
    if(running)
    {
        urlLinkParse.reStart(); //清空链接分析 url 队列,并将起始站点 url 读入到 这个队列。
    }
    else
    {
        start();
    }
}

初一看, start 方法好像只会被调用一次,即使 reStart 方法会被别的线程调用也不会有影响。其实多想一下答案就出来了,这个地方没有同步,这就是罪魁祸首!主线程会启动 autoRestart这个线程,同时主线程也会向下执行,调用到 start 方法,进行 if 判断,这是一条执行到 start 的路径。另一条是被主线程启动的 autoRestart 线程会执行 reStart,此时,running 还是 false(我们假设主线程还没有使Spider run 起来,故 running 是 false),则它还会继续向下执行,调用  start 方法,并再次进行 if 判断。此时,就有两个线程进行 if 判断,进入 if 执行体处理完毕后,会改变 if  条件,使其它线程进不来。但这时已经晚了,autoRestart  线程已经进来了,所以它又会傻呼呼的调用  start 方法(或者autoRestart先调用这个 start 方法,而 主线程在后面调用)。于是就出现了前文描述的情况。

把问题搞清楚了,问题就很好解决了,直接把所有线程类的 start 方法加上 synchronized 关键字同步即可。这是最基本的解决方法,实际上如果有很多线程会竞争某一个临界区,应该用两层 if 加上中间同步的结构,这一原因在我的一篇文章中已经描述了:http://blog.csdn.net/lizhihaoweiwei/article/details/20955045


总结:这原本应该是一个很简单的问题,但由于放入了较大的架构下面,较难看出来,只有跳出了局部,站在架构上面来看,才有可能找得到原因。或者发现一个问题原来就是个很难的过程,而再把它复述出来,一张图就解释清楚了,但发现这个问题的过程并不就是画一张图,理清架构的功夫,因为即使我勉励自己说,以后找问题要从架构上面来找,但又如何这样做呢?不可能一出现问题立即就从架构上找原因,这样何其繁琐。我们都知道找到“问题”不是个无解的问题, 我觉得更多的是在于经验,经验到了,很多问题一目了然。经验在于积累。这也是本文存在的目的。









  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值