gitbook鏈接:用python帶你進入AI中的深度學習技術領域https://www.gitbook.com/book/scrappyzhang/python_to_deeplearn/details
github鏈接:https://github.com/ScrappyZhang/python_web_Crawler_DA_ML_DL
6 協(xié)程
6.1 協(xié)程
協(xié)程柠衅,又稱微線程,纖程籍琳。英文名Coroutine菲宴。
協(xié)程的概念很早就提出來了,但直到最近幾年才在某些語言(如Lua)中得到廣泛應用趋急。
子程序喝峦,或者稱為函數,在所有語言中都是層級調用呜达,比如A調用B谣蠢,B在執(zhí)行過程中又調用了C,C執(zhí)行完畢返回查近,B執(zhí)行完畢返回眉踱,最后是A執(zhí)行完畢。
所以子程序調用是通過棧實現的霜威,一個線程就是執(zhí)行一個子程序谈喳。
子程序調用總是一個入口,一次返回侥祭,調用順序是明確的叁执。而協(xié)程的調用和子程序不同茄厘。
協(xié)程看上去也是子程序,但執(zhí)行過程中谈宛,在子程序內部可中斷次哈,然后轉而執(zhí)行別的子程序,在適當的時候再返回來接著執(zhí)行吆录。
注意窑滞,在一個子程序中中斷,去執(zhí)行其他子程序恢筝,不是函數調用哀卫,有點類似CPU的中斷。比如子程序A撬槽、B:
def A():
print('1')
print('2')
print('3')
def B():
print('4')
print('5')
print('6')
正常情況下此改,會輸出123456 。假設由協(xié)程執(zhí)行侄柔,在執(zhí)行A的過程中共啃,可以隨時中斷,去執(zhí)行B暂题,B也可能在執(zhí)行過程中中斷再去執(zhí)行A移剪,結果可能是:
1
2
4
5
3
6
但是在A中是沒有調用B的,所以協(xié)程的調用比函數調用理解起來要難一些薪者。
看起來A纵苛、B的執(zhí)行有點像多線程,但協(xié)程的特點在于是一個線程執(zhí)行言津,那和多線程比攻人,協(xié)程有何優(yōu)勢?
最大的優(yōu)勢就是協(xié)程極高的執(zhí)行效率纺念。因為子程序切換不是線程切換贝椿,而是由程序自身控制,因此陷谱,沒有線程切換的開銷烙博,和多線程比,線程數量越多烟逊,協(xié)程的性能優(yōu)勢就越明顯渣窜。
第二大優(yōu)勢就是不需要多線程的鎖機制,因為只有一個線程宪躯,也不存在同時寫變量沖突乔宿,在協(xié)程中控制共享資源不加鎖,只需要判斷狀態(tài)就好了访雪,所以執(zhí)行效率比多線程高很多详瑞。
因為協(xié)程是一個線程執(zhí)行掂林,那怎么利用多核CPU呢?最簡單的方法是多進程+協(xié)程坝橡,既充分利用多核泻帮,又充分發(fā)揮協(xié)程的高效率,可獲得極高的性能计寇。
注:在實現多任務時, 線程切換從系統(tǒng)層面遠不止保存和恢復 CPU上下文這么簡單锣杂。 操作系統(tǒng)為了程序運行的高效性每個線程都有自己緩存Cache等等數據,操作系統(tǒng)還會幫你做這些數據的恢復操作番宁。 所以線程的切換非常耗性能元莫。但是協(xié)程的切換只是單純的操作CPU的上下文,所以一秒鐘切換個上百萬次系統(tǒng)都抗的住蝶押。
6.2 python通過生成器實現協(xié)程
Python對協(xié)程的支持是通過generator生成器實現的踱蠢。在generator生成器中,我們不但可以通過for
循環(huán)來迭代棋电,還可以不斷調用next()
函數獲取由yield
語句返回的下一個值朽基。Python的yield
不但可以返回一個值,它還可以接收調用者發(fā)出的參數离陶。
yield的作用
掛起當前函數,將yield后面的值當做返回給調用生成器的地方衅檀;能夠在喚醒生成器函數的時候招刨,回復代碼繼續(xù)緊接著從上次執(zhí)行的地方執(zhí)行(可以接受額外的參數)
'''net05_yield.py'''
import time
def sing():
for i in range(5):
print('正在唱歌呢 %d' % i)
yield
time.sleep(1)
def dance():
for i in range(5):
print('正在跳舞呢 %d' % i)
yield
time.sleep(1)
if __name__ == '__main__':
s1 = sing() # 唱歌
d1 = dance() # 跳舞
i = 5
while i > 0:
next(s1) # next獲取由yield語句的協(xié)程切換
next(d1)
i -= 1
結果如下:
正在唱歌呢 0
正在跳舞呢 0
正在唱歌呢 1
正在跳舞呢 1
正在唱歌呢 2
正在跳舞呢 2
正在唱歌呢 3
正在跳舞呢 3
正在唱歌呢 4
正在跳舞呢 4
首先,我們應當注意到代碼中的sing和dance函數中的for循環(huán)是一個生成器哀军,這是python協(xié)程的前提沉眶。通過yield實現協(xié)程切換,next來調用完成各生成器的下一步動作杉适。整個過程在一個線程內完成谎倔,非常高效;不需要多線程的鎖猿推,不存在線程安全問題片习。
需要注意的是:在用yield來完成send參數傳遞時需要先執(zhí)行一次next,然后才可以send傳遞參數蹬叭∨河剑可以看例子:
在第一次喚醒生成器代碼時,我們使用next(f)秽五。在后續(xù)的協(xié)程切換中孽查,我們使用f.send(100)來講參數100傳遞給gen中的temp;通過value = f.send()將yield返回的值i賦給value坦喘。
'''net05_yield_variable.py'''
def gen():
i = 0
while i < 5:
temp = yield i
print('send過來的值為', temp)
i += 1
f = gen()
# 在第一次喚醒生成器代碼的時候 必須使用next(f) -- 在生成器代碼第一次執(zhí)行的時候 沒有可以接收參數的功能
print('第一次傳遞過來的值為', next(f))
while True:
try:
# value = next(f)
value = f.send(100)
except Exception as e:
print('結束')
break
else:
print("傳遞過來元素的值是%d" % value)
finally:
pass
結果:
第一次傳遞過來的值為 0
send過來的值為 100
傳遞過來元素的值是1
send過來的值為 100
傳遞過來元素的值是2
send過來的值為 100
傳遞過來元素的值是3
send過來的值為 100
傳遞過來元素的值是4
send過來的值為 100
結束
6.3 協(xié)程——greenlet
為了更好使用協(xié)程來完成多任務盲再,python中的greenlet模塊對其協(xié)程進行了封裝西设,從而省去next等使得切換任務變的更加簡單。我們可以通過pip install greenlet
安裝并使用它答朋。
它一般通過創(chuàng)建greenlet對象贷揽,并在相應的代碼塊里假如switch語句來實現不同函數間的切換。來繼續(xù)修改唱歌跳舞例子:
'''net05_greenlet.py'''
import time
from greenlet import greenlet # 導入greenlet.greenlet
def sing():
for i in range(5):
print('正在唱歌呢 %d' % i)
d1.switch() # 切換到跳舞函數
time.sleep(1)
def dance():
for i in range(5):
print('正在跳舞呢 %d' % i)
s1.switch() # 切換到唱歌函數
time.sleep(1)
if __name__ == '__main__':
s1 = greenlet(sing) # 唱歌
d1 = greenlet(dance) # 跳舞
s1.switch() # 切換到唱歌函數
結果如下:
正在唱歌呢 0
正在跳舞呢 0
正在唱歌呢 1
正在跳舞呢 1
正在唱歌呢 2
正在跳舞呢 2
正在唱歌呢 3
正在跳舞呢 3
正在唱歌呢 4
正在跳舞呢 4
我們首先創(chuàng)建了兩個greenlet實例對象绿映,然后從主程序通過s1.switch()切換到sing函數進行唱歌模塊擒滑。在sing函數中我們又通過d1.switch()切換到跳舞函數模塊;在dance函數中通過s1.switch()切換到sing函數叉弦。這樣便實現了交替切換執(zhí)行丐一。就像我們分析的那樣,它確實簡化了next等操作淹冰,但是需要開發(fā)者手動設置switch來實現不同函數之間的切換库车。
6.4 協(xié)程——gevent
正如上一節(jié)所說,greenlet需要手動設置切換樱拴,并不友好柠衍,所以本節(jié)介紹一個更友好的協(xié)程模塊gevent。我們可能需要通過pip install gevent
來安裝它晶乔。
gevent原理是當一個greenlet遇到IO(指的是input output 輸入輸出珍坊,比如網絡、文件操作等)操作時正罢,比如訪問網絡阵漏,就自動切換到其他的greenlet,等到IO操作完成翻具,再在適當的時候切換回來繼續(xù)執(zhí)行履怯。由于IO操作非常耗時,經常使程序處于等待狀態(tài)裆泳,有了gevent為我們自動切換協(xié)程叹洲,就保證總有greenlet在運行,而不是等待IO工禾。
gevent一般通過以下語句創(chuàng)建協(xié)程并執(zhí)行:
gevent.spawn(函數名运提,參數)
但是它創(chuàng)建的協(xié)程默認不自動切換,需要使用gevent包的monkey來進行破解切換闻葵,語句如下:
from gevent import monkey
monkey.patch_all()
我們繼續(xù)修改我們的唱歌跳舞實例糙捺,以gevent協(xié)程的方式來實現同時唱歌跳舞:
'''net05_gevent.py'''
import time
import gevent
# 默認協(xié)程不切換,需要使用monkey此語句來破解
from gevent import monkey
monkey.patch_all()
def sing():
for i in range(5):
print('正在唱歌呢 %d' % i)
time.sleep(1)
def dance():
for i in range(5):
print('正在跳舞呢 %d' % i)
time.sleep(1)
if __name__ == '__main__':
g1 = gevent.spawn(sing)
g2 = gevent.spawn(dance)
g1.join()
g2.join()
結果是一樣的笙隙,至此洪灯,我們分別通過多線程、多進程和協(xié)程三種方式實現了同時唱歌跳舞。
正在唱歌呢 0
正在跳舞呢 0
正在唱歌呢 1
正在跳舞呢 1
正在唱歌呢 2
正在跳舞呢 2
正在唱歌呢 3
正在跳舞呢 3
正在唱歌呢 4
正在跳舞呢 4
6.5 進程签钩、線程掏呼、協(xié)程區(qū)別
- 進程是資源分配的單位
- 線程是操作系統(tǒng)調度的單位
- 進程切換需要的資源很最大,效率很低
- 線程切換需要的資源一般铅檩,效率一般
- 協(xié)程切換任務資源很小憎夷,效率高
- 多進程、多線程根據cpu核數不一樣可能是并行的 也可能是并發(fā)的昧旨。協(xié)程的本質就是使用當前進程在不同的函數代碼中切換執(zhí)行拾给,可以理解為并行。 協(xié)程是一個用戶層面的概念兔沃,不同協(xié)程的模型實現可能是單線程 也可能是多線程蒋得。
6.7 協(xié)程實現網頁并發(fā)下載
需求實現:
通過gevent協(xié)程來同時下載百度、163乒疏、hao123的主頁html并保存到本地额衙。
完整源代碼:
'''net05_html_download.py'''
from gevent import monkey
import gevent
import urllib.request
monkey.patch_all()
def my_download(url):
print('GET: %s' % url)
resp = urllib.request.urlopen(url)
data = resp.read()
input_file = url.lstrip('http://www.').rstrip('.com/') + '.html'
with open(input_file, 'wb') as html_in_file:
html_in_file.write(data)
print('%d bytes received from %s.' % (len(data), url))
# joinall 為阻塞主程序使得列表內所有協(xié)程完成
gevent.joinall([
gevent.spawn(my_download, 'http://www.baidu.com/'),
gevent.spawn(my_download, 'http://www.163.com/'),
gevent.spawn(my_download, 'http://www.hao123.com/')
])