Python 協(xié)程的基本概念

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ì)探究痴柔。

誰說代碼不快樂,掃碼關(guān)注疫向,聽猿哥叨叨咳蔚,做一個(gè)快樂的程序猿豪嚎。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市谈火,隨后出現(xiàn)的幾起案子侈询,更是在濱河造成了極大的恐慌,老刑警劉巖糯耍,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扔字,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡温技,警方通過查閱死者的電腦和手機(jī)革为,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舵鳞,“玉大人震檩,你說我怎么就攤上這事◎讯椋” “怎么了抛虏?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)套才。 經(jīng)常有香客問我迂猴,道長(zhǎng),這世上最難降的妖魔是什么背伴? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任沸毁,我火速辦了婚禮,結(jié)果婚禮上傻寂,老公的妹妹穿的比我還像新娘以清。我一直安慰自己,他們只是感情好崎逃,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著眉孩,像睡著了一般个绍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上浪汪,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天巴柿,我揣著相機(jī)與錄音,去河邊找鬼死遭。 笑死广恢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的呀潭。 我是一名探鬼主播钉迷,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼至非,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了糠聪?” 一聲冷哼從身側(cè)響起荒椭,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎舰蟆,沒想到半個(gè)月后趣惠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡身害,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年味悄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塌鸯。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡侍瑟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出界赔,到底是詐尸還是另有隱情丢习,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布淮悼,位于F島的核電站咐低,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏袜腥。R本人自食惡果不足惜见擦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望羹令。 院中可真熱鬧鲤屡,春花似錦、人聲如沸福侈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肪凛。三九已至堰汉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間伟墙,已是汗流浹背翘鸭。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留戳葵,地道東北人就乓。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親生蚁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子噩翠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容