一个真正有知识的人的成长过程就像麦穗的生长过程:麦穗空了,麦子长得快,麦穗傲然挺立,但是,当麦穗熟透了,开始谦卑垂芒。——蒙田《蒙田随笔全集》部分《Python 多线程鸡年不鸡肋》讨论了python多线程是不是鸡肋的问题,得到了一些网友的认可。多线程在你面前是鸡肋。嗯,这点我也同意,不过我上一篇文章讨论的重点不是多线程和协程的比较,而是在IO密集型程序中,多线程还是有用的。关于协程,我说过它的效率比不上多线程,但是我对它了解的不多,所以这几天参考了一些资料,学习整理了一下,分享一下它在这里仅供您参考。如有错误请指正,谢谢。免责声明:本文介绍的协程属于入门级,请绕道而行,谨防落坑。文章思路:本文将首先介绍协程的概念,然后介绍协程在Python2.x和3.x下的使用,最后将协程与多线程进行比较,介绍异步爬虫模块。协程概念协程,又称微线程、纤程,英文名Coroutine。协程的作用是在执行函数A时,可以随时中断执行函数B,然后中断继续执行函数A(可以自由切换)。但是这个过程并不是函数调用(没有call语句),整个过程看起来是多线程的,但是只有一个线程在执行协程。优点执行效率极高,因为子程序的切换(函数)不是线程切换,是由程序自己控制的,没有切换线程的开销。因此,与多线程相比,线程越多,协程的性能优势就越明显。不需要多线程加锁机制,因为只有一个线程,不存在同时写变量的冲突,控制共享资源时也不需要加锁,执行效率高更高。解释:协程可以处理IO密集型程序的效率问题,但是处理CPU密集型程序并不是它的强项。如果想充分利用CPU利用率,可以结合多进程+协程。以上只是协程的一些概念,听起来可能比较抽象,下面结合代码说一下。这里主要介绍协程在Python中的应用。Python2对协程的支持有限。发电机的收益部分实现但不完整。gevent模块有更好的实现;Python3.4之后引入了asyncio模块,可以很好的利用协程。Python2.x协程python2.x协程应用:yieldgeventpython2.x中支持协程的模块不多,gevent是比较常用的,这里简单介绍一下gevent的用法。Geventgevent是一个通过greenlets实现协程的第三方库。其基本思想是:当一个greenlet遇到IO操作,比如访问网络,自动切换到其他greenlet,等待IO操作完成,在合适的时候切换回来继续执行。因为IO操作是非常耗时的,所以程序经常处于等待状态。通过gevent自动为我们切换协程,保证了一直有greenlet在运行,而不是等待IO。installpipinstallgevent***版本好像是支持windows的。之前的测试好像不能在windows上运行...用法先来看一个简单的爬虫例子:#!-*-coding:utf-8-*-importgeventfromgeventimportmonkey;monkey.patch_all()importurllib2defget_body(i):print"start",iurllib2.urlopen("http://cn.bing.com")print"end",itasks=[gevent.spawn(get_body,i)foriinrange(3)]gevent.joinall(tasks)运行结果:start0start1start2end2end0end1解释:从结果来看,执行get_body的顺序应该是先输出“start”,然后运行到urllib2,遇到IO阻塞会自动切换到运行下一个程序(继续执行get_body输出start),执行结束,直到urllib2返回结果。也就是说,程序并没有等待urllib2请求网站返回结果,而是直接先跳过,等待执行完成再返回获取返回值。值得一提的是,在这个过程中,只有一个线程在执行,所以这和多线程的概念是不一样的。看一下多线程代码:importthreadingimporturllib2defget_body(i):print"start",iurllib2.urlopen("http://cn.bing.com")print"end",iforiinrange(3):t=threading。Thread(target=get_body,args=(i,))t.start()运行结果:start0start1start2end1end2end0说明:从结果来看,多线程和协程的效果是一样的,都实现了IO阻塞时切换的功能。不同的是,多线程切换是线程(线程间切换),而协程切换是上下文(可以理解为执行的函数)。切换线程的代价明显大于切换上下文的代价,所以当线程比较多的时候,协程的效率要高于多线程。(猜猜多进程的切换开销应该是最低的)gevent指令monkey可以让一些阻塞的模块畅通,机制:遇到IO操作自动切换,手动切换可以使用gevent.sleep(0)(把爬虫代码换成this,效果和切换上下文一样)gevent.spawn启动协程,参数为函数名,参数名gevent.joinall停止协程Python3.xcoroutine为了测试Python3下的协程应用。x,我在virtualenv下安装了python3.6环境。python3.x协程应用:asynico+yieldfrom(python3.4)asynico+await(python3.5)geventPython3.4之后引入了asyncio模块,可以很好的支持协程。asyncoasyncio是Python3.4引入的标准库,直接支持异步IO。asyncio的异步操作需要通过协程中的yieldfrom来完成。使用示例:(需要在python3.4之后使用)importasyncio@asyncio.coroutinedeftest(i):print("test_1",i)r=yieldfromasyncio.sleep(1)print("test_2",i)loop=asyncio.get_event_loop()tasks=[test(i)foriinrange(5)]loop.run_until_complete(asyncio.wait(tasks))loop.close()运行结果:test_13test_14test_10test_11test_12test_23test_20test_22test_24test_21说明:从运行结果可以看出是一样的和gevent实现的效果一样,遇到IO操作也会switch(所以先输出test_1,输出test_1后再输出test_2)。但是这里我有点不清楚,为什么test_1的输出没有按顺序执行呢?可以对比gevent的输出结果(希望高手解答)。asyncio解释说@asyncio.coroutine把一个generator标记为coroutine类型,然后我们把这个coroutine丢到EventLoop中去执行。test()会先打印出test_1,然后,yieldfrom语法可以让我们方便地调用另一个生成器。由于asyncio.sleep()也是协程,线程不会等待asyncio.sleep(),而是直接中断执行下一个消息循环。当asyncio.sleep()返回时,线程可以从yieldfrom中获取返回值(这里是None),然后执行下一行语句。将asyncio.sleep(1)视为需要1秒的IO操作。在此期间主线程不等待,而是在EventLoop中执行其他可执行的协程,因此可以实现并发执行。asynco/await为了简化和更好地识别异步IO,从Python3.5开始引入了新的语法async和await,可以让coroutine的代码更加简洁易读。请注意,async和await是协程的新语法。要使用新的语法,只需要做两个简单的替换:将@asyncio.coroutine替换为async;将yieldfrom替换为await。使用示例(用于python3.5之后的版本):importasyncioasyncdeftest(i):print("test_1",i)awaitasyncio.sleep(1)print("test_2",i)loop=asyncio.get_event_loop()tasks=[test(i)foriinrange(5)]loop.run_until_complete(asyncio.wait(tasks))loop.close()运行结果同前。说明:与上一节相比,此处仅将yieldfrom替换为await,将@asyncio.coroutine替换为async,其余不变。gevent与python2.x相同。CoroutineVS多线程如果你已经通过上面的介绍了解了多线程和coroutine的区别,那我觉得就没必要考了。因为当线程越来越多的时候,多线程的主要开销都花在了线程切换上,而协程是在一个线程内切换,所以开销要小很多。这可能是两者在性能上的根本区别。(个人观点)异步爬虫可能比较关心协程的朋友,大部分都是用它来写爬虫(因为协程可以很好的解决IO阻塞问题),但是我发现常用的urllib和requests不能和asyncio结合使用,它可能是因为爬虫模块本身是同步的(可能是我没找到用法)。那么针对异步爬虫的需求,如何使用协程呢?或者如何编写异步爬虫?下面介绍几个我知道的解决方案:grequests(requests模块的异步化)爬虫模块+gevent(推荐)aiohttp(好像这方面资料不多,暂时不知道怎么用)asyncio内置的爬虫功能(这个也不好用)协程池功能:控制协程数量frombs4importBeautifulSoupimportrequestsimportgeventfromgeventimportmonkey,poolmonkey.patch_all()jobs=[]links=[]p=pool.Pool(10)urls=['http://www.google.com',#...another100urls]defget_links(url):r=requests.get(url)ifr.status_code==200:soup=BeautifulSoup(r.text)links+soup.find_all('a')forurlinurls:jobs.append(p.spawn(get_links,url))gevent.joinall(jobs)本文为一些自学笔记,分享给新手朋友,仅供参考文章学习渠道:Pythonmulti-进程(http://python.jobbole.com/87760/)Python多线程(http://python.jobbole.com/87772/)
