真正有知識的人的成長過程,就像麥穗的成長過程:麥穗空的時候,麥子長得很快,麥穗驕傲地高高昂起,可是,麥穗成熟飽滿時,它們開始謙虛,垂下麥芒。python
——蒙田《蒙田隨筆全集》
上篇論述了關於python多線程是不是雞肋的問題,獲得了一些網友的承認,固然也有一些不一樣意見,表示協程比多線程不知強多少,在協程面前多線程算是雞肋。好吧,對此我也表示贊同,然而上篇我論述的觀點不在於多線程與協程的比較,而是在於IO密集型程序中,多線程尚有用武之地。windows
對於協程,我表示其效率確非多線程能比,但本人對此瞭解並不深刻,所以最近幾日參考了一些資料,學習整理了一番,在此分享出來僅供你們參考,若有謬誤請指正,多謝。python3.x
申明:本文介紹的協程是入門級別,大神請繞道而行,謹防入坑。網絡
文章思路:本文將先介紹協程的概念,而後分別介紹Python2.x與3.x下協程的用法,最終將協程與多線程作比較並介紹異步爬蟲模塊。多線程
協程,又稱微線程,纖程,英文名Coroutine。協程的做用,是在執行函數A時,能夠隨時中斷,去執行函數B,而後中斷繼續執行函數A(能夠自由切換)。但這一過程並非函數調用(沒有調用語句),這一整個過程看似像多線程,然而協程只有一個線程執行。併發
說明:協程能夠處理IO密集型程序的效率問題,可是處理CPU密集型不是它的長處,如要充分發揮CPU利用率能夠結合多進程+協程。app
以上只是協程的一些概念,可能聽起來比較抽象,那麼我結合代碼講一講吧。這裏主要介紹協程在Python的應用,Python2對協程的支持比較有限,生成器的yield實現了一部分但不徹底,gevent模塊卻是有比較好的實現;Python3.4之後引入了asyncio模塊,能夠很好的使用協程。異步
python2.x協程應用:async
python2.x中支持協程的模塊很少,gevent算是比較經常使用的,這裏就簡單介紹一下gevent的用法。函數
gevent是第三方庫,經過greenlet實現協程,其基本思想:
當一個greenlet遇到IO操做時,好比訪問網絡,就自動切換到其餘的greenlet,等到IO操做完成,再在適當的時候切換回來繼續執行。因爲IO操做很是耗時,常常使程序處於等待狀態,有了gevent爲咱們自動切換協程,就保證總有greenlet在運行,而不是等待IO。
pip install gevent
最新版貌似支持windows了,以前測試好像windows上運行不了……
首先來看一個簡單的爬蟲例子:
#! -*- coding:utf-8 -*- import gevent from gevent import monkey;monkey.patch_all() import urllib2 def get_body(i): print "start",i urllib2.urlopen("http://cn.bing.com") print "end",i tasks=[gevent.spawn(get_body,i) for i in range(3)] gevent.joinall(tasks)
運行結果:
start 0 start 1 start 2 end 2 end 0 end 1
說明:從結果上來看,執行get_body的順序應該先是輸出」start」,而後執行到urllib2時碰到IO堵塞,則會自動切換運行下一個程序(繼續執行get_body輸出start),直到urllib2返回結果,再執行end。也就是說,程序沒有等待urllib2請求網站返回結果,而是直接先跳過了,等待執行完畢再回來獲取返回值。值得一提的是,在此過程當中,只有一個線程在執行,所以這與多線程的概念是不同的。
換成多線程的代碼看看:
import threading import urllib2 def get_body(i): print "start",i urllib2.urlopen("http://cn.bing.com") print "end",i for i in range(3): t=threading.Thread(target=get_body,args=(i,)) t.start()
運行結果:
start 0 start 1 start 2 end 1 end 2 end 0
說明:從結果來看,多線程與協程的效果同樣,都是達到了IO阻塞時切換的功能。不一樣的是,多線程切換的是線程(線程間切換),協程切換的是上下文(能夠理解爲執行的函數)。而切換線程的開銷明顯是要大於切換上下文的開銷,所以當線程越多,協程的效率就越比多線程的高。(猜測多進程的切換開銷應該是最大的)
python3.5協程使用能夠移步:Python3.5協程學習研究
爲了測試Python3.x下的協程應用,我在virtualenv下安裝了python3.6的環境。
python3.x協程應用:
Python3.4之後引入了asyncio模塊,能夠很好的支持協程。
asyncio是Python 3.4版本引入的標準庫,直接內置了對異步IO的支持。asyncio的異步操做,須要在coroutine中經過yield from完成。
例子:(需在python3.4之後版本使用)
import asyncio @asyncio.coroutine def test(i): print("test_1",i) r=yield from asyncio.sleep(1) print("test_2",i) loop=asyncio.get_event_loop() tasks=[test(i) for i in range(5)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
運行結果:
test_1 3 test_1 4 test_1 0 test_1 1 test_1 2 test_2 3 test_2 0 test_2 2 test_2 4 test_2 1
說明:從運行結果能夠看到,跟gevent達到的效果同樣,也是在遇到IO操做時進行切換(因此先輸出test_1,等test_1輸出完再輸出test_2)。但此處我有一點不明,test_1的輸出爲何不是按照順序執行的呢?能夠對比gevent的輸出結果(但願大神能解答一下)。
@asyncio.coroutine把一個generator標記爲coroutine類型,而後,咱們就把這個coroutine扔到EventLoop中執行。
test()會首先打印出test_1,而後,yield from語法可讓咱們方便地調用另外一個generator。因爲asyncio.sleep()也是一個coroutine,因此線程不會等待asyncio.sleep(),而是直接中斷並執行下一個消息循環。當asyncio.sleep()返回時,線程就能夠從yield from拿到返回值(此處是None),而後接着執行下一行語句。
把asyncio.sleep(1)當作是一個耗時1秒的IO操做,在此期間,主線程並未等待,而是去執行EventLoop中其餘能夠執行的coroutine了,所以能夠實現併發執行。
爲了簡化並更好地標識異步IO,從Python 3.5開始引入了新的語法async和await,可讓coroutine的代碼更簡潔易讀。
請注意,async和await是針對coroutine的新語法,要使用新的語法,只須要作兩步簡單的替換:
例子(python3.5之後版本使用):
import asyncio async def test(i): print("test_1",i) await asyncio.sleep(1) print("test_2",i) loop=asyncio.get_event_loop() tasks=[test(i) for i in range(5)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
運行結果與以前一致。
說明:與前一節相比,這裏只是把yield from換成了await,@asyncio.coroutine換成了async,其他不變。
同python2.x用法同樣。
若是經過以上介紹,你已經明白多線程與協程的不一樣之處,那麼我想測試也就沒有必要了。由於當線程愈來愈多時,多線程主要的開銷花費在線程切換上,而協程是在一個線程內切換的,所以開銷小不少,這也許就是二者性能的根本差別之處吧。(我的觀點)
也許關心協程的朋友,大部分是用其寫爬蟲(由於協程能很好的解決IO阻塞問題),然而我發現經常使用的urllib、requests沒法與asyncio結合使用,多是由於爬蟲模塊自己是同步的(也多是我沒找到用法)。那麼對於異步爬蟲的需求,又該怎麼使用協程呢?或者說怎麼編寫異步爬蟲?
給出幾個我所瞭解的方案:
做用:控制協程數量
from bs4 import BeautifulSoup import requests import gevent from gevent import monkey, pool monkey.patch_all() jobs = [] links = [] p = pool.Pool(10) urls = [ 'http://www.google.com', # ... another 100 urls ] def get_links(url): r = requests.get(url) if r.status_code == 200: soup = BeautifulSoup(r.text) links + soup.find_all('a') for url in urls: jobs.append(p.spawn(get_links, url)) gevent.joinall(jobs)