Python 協(xié)程的基本概念
在學(xué)習(xí) Python 基礎(chǔ)的過程中,遇到了比較難理解的地方三圆,那就是協(xié)程谁撼。剛開始看了廖雪峰老師的博客,沒怎么看懂镶骗,后面自己多方位 google 了一下桶现,再回來看,終于看出了點(diǎn)眉目鼎姊,在此總結(jié)下骡和。
什么是 yield 和 yield from
yield
在學(xué)習(xí)協(xié)程之前相赁,要先搞懂幾個(gè)基本語法,那就是 yield 和 yield from慰于,這也是陸續(xù)困擾我?guī)滋斓膯栴}钮科,等這兩個(gè)概念弄懂以后,后面的事情就比較簡(jiǎn)單了婆赠。
- yield 是一個(gè)關(guān)鍵字绵脯,當(dāng)一個(gè)方法中帶有 yield 時(shí),它就不是一個(gè)普通的方法了休里,而是變成了一個(gè)所謂的“生成器”蛆挫。
- 生成器不會(huì)一下子把所有值都返回給你,可以使用 next() 方法來調(diào)用妙黍,來不斷取值璃吧。
- 當(dāng)生成器中執(zhí)行到 yield 的時(shí)候,會(huì)從 yield 處返回結(jié)果废境,并保留上下文,等下一次 next() 的時(shí)候筒繁,會(huì)從上次的 yield 處繼續(xù)執(zhí)行噩凹。
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
f = fib(10)
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))
上面是一個(gè)經(jīng)典的斐波那契數(shù)列的生成函數(shù),每次調(diào)用next都會(huì)從 yield 處返回結(jié)果毡咏。
輸出:
1
1
2
3
5
Traceback (most recent call last):
File "test.py", line 116, in <module>
print(next(f))
StopIteration: done
遇到 return 會(huì)拋出異常驮宴,并將 return 的值包含在異常中拋出來。
一般我們不會(huì)一直調(diào)用 next() , 而是使用 for 循環(huán):
for b in fib(5):
print(b)
輸出:
1
1
2
3
5
此處沒有拋出異常呕缭,原因還待解釋堵泽。。
使用 yield 一個(gè)一個(gè)地返回結(jié)果有什么作用恢总?那是因?yàn)檫@樣可以邊循環(huán)邊計(jì)算迎罗,邊返回結(jié)果,不用創(chuàng)建完整的 list 片仿,省去大量的內(nèi)存空間纹安。
接下來的關(guān)鍵點(diǎn),可以通過 yield 傳遞參數(shù)砂豌!這個(gè)地方我也是弄了好久才弄明白的厢岂。⊙艟啵看下面的生產(chǎn)者和消費(fèi)者的例子:
def customer():
r = '404 empty'
while True:
print('star consume ..')
n = yield r # 2
print('consuming {} ..'.format(n))
r = '200 ok'
def producer(c):
r = c.send(None) # 1
print(r) # 3
n = 0
while n < 10:
n += 1
print('producing {} ..'.format(n))
r = c.send(n) # 4
print('consumer {} return'.format(r))
c.close()
if __name__ == '__main__':
producer(customer())
輸出:
star consume ..
404 empty
producing 1 ..
consuming 1 ..
star consume ..
consumer 200 ok return
producing 2 ..
consuming 2 ..
star consume ..
consumer 200 ok return
producing 3 ..
consuming 3 ..
star consume ..
consumer 200 ok return
producing 4 ..
consuming 4 ..
star consume ..
consumer 200 ok return
producing 5 ..
consuming 5 ..
star consume ..
consumer 200 ok return
先說明一下塔粒,send 方法可以給 yield 發(fā)送參數(shù)。
程序剛開始執(zhí)行到“#1”處筐摘,這里必須先調(diào)用 send(None) 一下卒茬。此處可是有講究的船老,學(xué)名叫“預(yù)激”,作用是先啟動(dòng)一下生成器扬虚,讓它先卡在 yield 努隙,所以此時(shí)程序在“#2”處中斷了,并返回 r 辜昵,隨后“#3”處打印出 “404 empty” 荸镊。
接下來程序來到“#4”處,又調(diào)用了 send 方法堪置,此時(shí) send 參數(shù)為1躬存,所以“#2”處被重新激活,將 n 賦值為 1舀锨,然后繼續(xù)向下執(zhí)行岭洲。
接著又循環(huán)來到 “#2” 處,yield 將 r 返回坎匿,此處中斷盾剩,來到“#4”處繼續(xù)執(zhí)行。如此不斷循環(huán)替蔬,直到滿足條件退出循環(huán)告私。
像這樣,producer生產(chǎn)完承桥,告訴customer消費(fèi)驻粟,消費(fèi)完再通知producer生產(chǎn),這些事情都發(fā)生在同一個(gè)線程里面凶异,因此沒有多線程的鎖和資源爭(zhēng)奪的問題蜀撑。至此,協(xié)程的初步面貌就已經(jīng)浮出水面剩彬。
小 tip:
調(diào)用 send(None) 酷麦,就相當(dāng)于調(diào)用 next()。
yield from
Python3.3 版本的 PEP 380 中添加了 yield from 語法襟衰,PEP380 的標(biāo)題是 “syntax for delegating to subgenerator”(把指責(zé)委托給子生成器的句法)贴铜。由此我們可以知道,yield from 是可以實(shí)現(xiàn)嵌套生成器的使用瀑晒。
yield from x 表達(dá)式對(duì)x對(duì)象做的第一件事是绍坝,調(diào)用 iter(x),獲取迭代器苔悦。所以要求x是可迭代對(duì)象轩褐。
yield from 的主要功能是打開雙向通道,把最外層的調(diào)用方與最內(nèi)層的子生成器連接起來玖详,使兩者可以直接發(fā)送和產(chǎn)出值把介,還可以直接傳入異常勤讽,而不用在中間的協(xié)程添加異常處理的代碼。
這句話理解起來很麻煩拗踢,參考以下代碼:
def A():
yield from B()
yield from C()
def B():
yield '001'
yield '002'
yield '003'
def C():
yield '004'
yield '005'
if __name__ == '__main__':
for s in A():
print(s)
輸出:
001
002
003
004
005
由此可見脚牍,生成器 A 通過 yield from 將任務(wù)下發(fā)給了生成器 B 和生成器 C 來執(zhí)行了。yield from 可以很方便地拆分生成器巢墅,變?yōu)閹讉€(gè)小生成器诸狭,方便代碼管理。
什么是協(xié)程
根據(jù)維基百科給出的定義君纫,“協(xié)程 是為非搶占式多任務(wù)產(chǎn)生子程序的計(jì)算機(jī)程序組件驯遇,協(xié)程允許不同入口點(diǎn)在不同位置暫停或開始執(zhí)行程序”蓄髓。
換句話說就是你可以中斷函數(shù)執(zhí)行叉庐,轉(zhuǎn)而執(zhí)行別的函數(shù)。聽起來就像是你正在燒水会喝,在此期間你可以做下一個(gè)事情陡叠,而不用等水燒開。
以前我們都是用多線程和鎖來做這個(gè)事情肢执,但是時(shí)常會(huì)擔(dān)心線程安全和死鎖問題匾竿,多線程切換還會(huì)產(chǎn)生額外的開銷。現(xiàn)在使用協(xié)程蔚万,在一個(gè)線程里面就能完成這些任務(wù),不用擔(dān)心線程問題临庇,沒有使用任何鎖反璃,大大提高了執(zhí)行效率。
上面生產(chǎn)者和消費(fèi)者的例子假夺,使用了 yield 簡(jiǎn)單的實(shí)現(xiàn)了一個(gè)協(xié)程代碼淮蜈。
asyncio
asyncio是Python 3.4版本引入的標(biāo)準(zhǔn)庫,直接內(nèi)置了對(duì)異步IO的支持已卷。
asyncio的編程模型就是一個(gè)消息循環(huán)梧田。我們從asyncio模塊中直接獲取一個(gè)EventLoop的引用,然后把需要執(zhí)行的協(xié)程扔到EventLoop中執(zhí)行侧蘸,就實(shí)現(xiàn)了異步IO裁眯。
asyncio庫使我們方便地實(shí)現(xiàn)協(xié)程,我們可以把多個(gè)協(xié)程方法扔到asyncio的消息循環(huán)中讳癌,asyncio就自動(dòng)地幫我們調(diào)用并協(xié)調(diào)這些協(xié)程方法穿稳。
@asyncio.coroutine 裝飾器,可以幫助我們把一個(gè)生成器裝飾為協(xié)程方法晌坤。
import threading
import asyncio
@asyncio.coroutine
def hello(i):
print('Hello world! {} {}'.format(i, threading.currentThread()))
yield from asyncio.sleep(3)
print('Hello again! {} {}'.format(i, threading.currentThread()))
loop = asyncio.get_event_loop()
tasks = [hello(1), hello(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
輸出
Hello world! 2 <_MainThread(MainThread, started 140736287097792)>
Hello world! 1 <_MainThread(MainThread, started 140736287097792)>
// 中間停3秒
Hello again! 2 <_MainThread(MainThread, started 140736287097792)>
Hello again! 1 <_MainThread(MainThread, started 140736287097792)>
可以看到在第一個(gè)任務(wù)執(zhí)行時(shí)遇到 yield from 逢艘,這時(shí)候程序不會(huì)等著旦袋,而是馬上開始下一個(gè)任務(wù)。當(dāng)然先開始哪個(gè)任務(wù)是隨機(jī)的它改“淘校看打印出來的線程信息顯示,兩個(gè)任務(wù)是在同一個(gè)線程執(zhí)行央拖。
協(xié)程幫助我們?cè)谝粋€(gè)線程里面異步執(zhí)行多個(gè)任務(wù)祭阀,里面的任務(wù)是并發(fā)進(jìn)行的。
async/await
為了簡(jiǎn)化并更好地標(biāo)識(shí)異步 IO爬泥,從Python 3.5 開始引入了新的語法async和await柬讨,可以讓coroutine的代碼更簡(jiǎn)潔易讀。
使用也非常簡(jiǎn)單袍啡,只要把原來的 @asyncio.coroutine 替換為 async 踩官,把 yield from 替換為 await 。
修改上一節(jié)的代碼:
import threading
import asyncio
async def hello(i):
print('Hello world! {} {}'.format(i, threading.currentThread()))
await asyncio.sleep(3)
print('Hello again! {} {}'.format(i, threading.currentThread()))
loop = asyncio.get_event_loop()
tasks = [hello(1), hello(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
協(xié)程境输,有什么用蔗牡?
asyncio可以實(shí)現(xiàn)單線程并發(fā)IO操作。如果僅用在客戶端嗅剖,發(fā)揮的威力不大辩越。如果把a(bǔ)syncio用在服務(wù)器端,例如Web服務(wù)器信粮,由于HTTP連接就是IO操作黔攒,因此可以用單線程+coroutine實(shí)現(xiàn)多用戶的高并發(fā)支持。
aiohttp 應(yīng)運(yùn)而生强缘,它是由 asyncio 實(shí)現(xiàn)的 HTTP 框架督惰,幫助我們快速地搭建一個(gè)異步的 web 應(yīng)用。
使用它要先安裝:
pip install aiohttp
利用它我們用一小段代碼搭建一個(gè)小應(yīng)用:
訪問"http://127.0.0.1:8000/"根目錄旅掂,首頁返回
b'<h1>Index</h1>'
訪問"http://127.0.0.1:8000/hello/world"赏胚,根據(jù)URL參數(shù)返回文本hello, world!。
import asyncio
from aiohttp import web
async def index(request):
await asyncio.sleep(1)
return web.Response(body=b'<h1>Index</h1>', content_type='text/html')
async def hello(request):
await asyncio.sleep(1)
text = '<h1>hello, {}!</h1>'.format(request.match_info['name'])
return web.Response(body=text.encode('utf-8'), content_type='text/html')
async def init(loop):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', index)
app.router.add_route('GET', '/hello/{name}', hello)
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('Server started at http://127.0.0.1:8000...')
return srv
loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()
總結(jié)
在這里商虐,我們初步了解了下協(xié)程的基本概念觉阅。使用 yield 和 yield from ,并且利用 asyncio 庫秘车,可以組合成一個(gè)由協(xié)程組成的異步程序典勇。async/await 幫助我們簡(jiǎn)化協(xié)程代碼。利用協(xié)程可以寫出很強(qiáng)大的 web 應(yīng)用叮趴,進(jìn)一步的深入我們后面再細(xì)細(xì)探究痴柔。