在webmagic的多线程抓取中有一个比较麻烦的问题:当Scheduler拿不到url的时候,不能当即退出,须要等到没抓完的线程都运行完毕,没有新url产生时,才能退出。以前使用Thread.sleep来实现,当拿不到url时,sleep一段时间再取,肯定没有线程执行以后,再退出。html
可是这种方式始终不够优雅。Java里面有wait/notify机制能够解决这种同步的问题。因而webmagic 0.4.0用wait/notify机制代替了以前的Thread.sleep机制。代码以下:java
while (!Thread.currentThread().isInterrupted() && stat.get() == STAT_RUNNING) { Request request = scheduler.poll(this); if (request == null) { if (threadAlive.get() == 0 && exitWhenComplete) { break; } // wait until new url added waitNewUrl(); } else { final Request requestFinal = request; threadAlive.incrementAndGet(); executorService.execute(new Runnable() { @Override public void run() { try { processRequest(requestFinal); } catch (Exception e) { logger.error("download " + requestFinal + " error", e); } finally { threadAlive.decrementAndGet(); signalNewUrl(); } } }); } } private void waitNewUrl() { try { newUrlLock.lock(); try { newUrlCondition.await(); } catch (InterruptedException e) { } } finally { newUrlLock.unlock(); } }
这里当线程完成以后,会调用signalNewUrl()
来通知主线程,中止等待!git
0.4.0发布以后,有用户问我,为何有的时候抓完没法退出?我开始就怀疑这里可能存在线程安全问题,可是苦于没法复现。github
思考了一下,有可能存在这样执行状况:web
if (threadAlive.get() == 0 && exitWhenComplete)
check跳过,因而准备进入waitNewUrl()
;threadAlive.decrementAndGet();
和signalNewUrl();
相继执行;waitNewUrl()
,结果已无线程执行,也无人能够notify它了,因而线程一直等待…那么彷佛在lock里加入double-check就OK了?可是今天看了http://coolshell.cn/articles/4576.html这篇文章,大概意思是:出了问题不要靠猜!必定要复现并测试!shell
因而决定手动模拟!开启10个线程,并mock了全部部件,循环10000次去执行,代码不贴了,地址:https://github.com/code4craft/webmagic/blob/master/webmagic-core/src/test/java/us/codecraft/webmagic/SpiderTest.java。执行一下,果真到了第13次就卡住了!jstack以后,果真卡在newUrlCondition.await();
这里!安全
而后加入double-check:多线程
private void waitNewUrl() { try { newUrlLock.lock(); //double check if (threadAlive.get() == 0 && exitWhenComplete) { return; } try { newUrlCondition.await(); } catch (InterruptedException e) { } } finally { newUrlLock.unlock(); } }
结果执行成功!至此问题解决!ide
通过这个例子,也大体明白了为何wait/notify以前老是要先lock
。为何呢?有机会写一篇文章总结一下吧!测试
很简单,是吧?其实这篇文章只想说明一件事:出了bug不要靠猜!必定要复现并确认解决!