<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          信息量巨大!把 Python 協(xié)程的本質(zhì)扒得干干凈凈

          共 10130字,需瀏覽 21分鐘

           ·

          2021-10-02 22:20

          本文章信息量較大,從 IO 多路復(fù)用,到生成器的使用,再到?asyncawait?背后的實(shí)現(xiàn)原理,深入淺出,剖析得非常透徹,非常硬核!
          這兩天因?yàn)橐稽c(diǎn)個(gè)人原因?qū)懥它c(diǎn)好久沒(méi)碰的 Python ,其中涉及到「協(xié)程」編程,上次搞的時(shí)候,它還是 Web 框架 tornado 特有的 feature,現(xiàn)在已經(jīng)有 asyncawait 關(guān)鍵字支持了。思考了一下其實(shí)現(xiàn),回顧了下這些年的演變,覺得還有點(diǎn)意思。
          都是單線程,為什么原來(lái)低效率的代碼用了 asyncawait 加一些異步庫(kù)就變得效率高了?
          如果在做基于 Python 的網(wǎng)絡(luò)或者 Web 開發(fā)時(shí),對(duì)于這個(gè)問(wèn)題曾感到疑惑,這篇文章嘗試給一個(gè)答案。

          0x00 開始之前

          首先,本文不是帶你瀏覽源代碼,然后對(duì)照原始代碼給你講 Python 標(biāo)準(zhǔn)的實(shí)現(xiàn)。相反,我們會(huì)從實(shí)際問(wèn)題出發(fā),思考解決問(wèn)題的方案,一步步體會(huì)解決方案的演進(jìn)路徑,最重要的,希望能在過(guò)程中獲得知識(shí)系統(tǒng)性提升。
          ?? 本文僅是提供了一個(gè)獨(dú)立的思考方向,并未遵循歷史和現(xiàn)有實(shí)際具體的實(shí)現(xiàn)細(xì)節(jié)。
          其次,閱讀這篇文章需要你對(duì) Python 比較熟悉,至少了解 Python 中的生成器 generator 的概念。

          0x01 IO 多路復(fù)用

          這是性能的關(guān)鍵。但我們這里只解釋概念,其實(shí)現(xiàn)細(xì)節(jié)不是重點(diǎn),這對(duì)我們理解 Python 的協(xié)程已經(jīng)足夠了,如已足夠了解,前進(jìn)到 0x02
          首先,你要知道所有的網(wǎng)絡(luò)服務(wù)程序都是一個(gè)巨大的死循環(huán),你的業(yè)務(wù)邏輯都在這個(gè)循環(huán)的某個(gè)時(shí)刻被調(diào)用:
          def?handler(request):
          ????#?處理請(qǐng)求
          ????pass

          #?你的?handler?運(yùn)行在?while?循環(huán)中
          while?True:
          ????#?獲取一個(gè)新請(qǐng)求
          ????request?=?accept()
          ????#?根據(jù)路由映射獲取到用戶寫的業(yè)務(wù)邏輯函數(shù)
          ????handler?=?get_handler(request)
          ????#?運(yùn)行用戶的handler,處理請(qǐng)求
          ????handler(request)
          設(shè)想你的 Web 服務(wù)的某個(gè) handler,在接收到請(qǐng)求后需要一個(gè) API 調(diào)用才能響應(yīng)結(jié)果。
          對(duì)于最傳統(tǒng)的網(wǎng)絡(luò)應(yīng)用,你的 API 請(qǐng)求發(fā)出去后在等待響應(yīng),此時(shí)程序停止運(yùn)行,甚至新的請(qǐng)求也得在響應(yīng)結(jié)束后才進(jìn)得來(lái)。如果你依賴的 API 請(qǐng)求網(wǎng)絡(luò)丟包嚴(yán)重,響應(yīng)特別慢呢?那應(yīng)用的吞吐量將非常低。
          很多傳統(tǒng) Web 服務(wù)器使用多線程技術(shù)解決這個(gè)問(wèn)題:把 handler 的運(yùn)行放到其他線程上,每個(gè)線程處理一個(gè)請(qǐng)求,本線程阻塞不影響新請(qǐng)求進(jìn)入。這能一定程度上解決問(wèn)題,但對(duì)于并發(fā)比較大的系統(tǒng),過(guò)多線程調(diào)度會(huì)帶來(lái)很大的性能開銷。
          IO 多路復(fù)用可以做到不使用線程解決問(wèn)題,它是由操作系統(tǒng)內(nèi)核提供的功能,可以說(shuō)專門為這類場(chǎng)景而生。簡(jiǎn)單來(lái)講,你的程序遇到網(wǎng)絡(luò)IO時(shí),告訴操作系統(tǒng)幫你盯著,同時(shí)操作系統(tǒng)提供給你一個(gè)方法,讓你可以隨時(shí)獲取到有哪些 IO 操作已經(jīng)完成。就像這樣:
          #?操作系統(tǒng)的IO復(fù)用示例偽代碼
          #?向操作系統(tǒng)IO注冊(cè)自己關(guān)注的IO操作的id和類型
          io_register(io_id,?io_type)
          io_register(io_id,?io_type)

          #?獲取完成的IO操作
          events?=?io_get_finished()

          for?(io_id,?io_type)?in?events:
          ????if?io_type?==?READ:
          ????????data?=?read_data(io_id)?
          ????elif?io_type?==?WRITE:
          ????????write_data(io_id,data)
          把 IO 復(fù)用邏輯融合到我們的服務(wù)器中,大概會(huì)像這樣:
          call_backs?=?{}

          def?handler(req):
          ????#?do?jobs?here
          ????io_register(io_id,?io_type)
          ????def?call_back(result):
          ????????#?使用返回的result完成剩余工作...
          ????call_backs[io_id]?=?call_back

          #?新的循環(huán)
          while?True
          ????#?獲取已經(jīng)完成的io事件
          ????events?=?io_get_finished()
          ????for?(io_id,?io_type)?in?events:
          ????????if?io_type?==?READ:?#?讀取
          ????????????data?=?read(io_id)?
          ????????????call_back?=?call_backs[io_id]
          ????????????call_back(data)
          ????????else:
          ????????????#?其他類型io事件的處理
          ????????????pass

          ????#?獲取一個(gè)新請(qǐng)求
          ????request?=?accept()
          ????#?根據(jù)路由映射獲取到用戶寫的業(yè)務(wù)邏輯函數(shù)
          ????handler?=?get_handler(request)
          ????#?運(yùn)行用戶的handler,處理請(qǐng)求
          ????handler(request)
          我們的 handler 對(duì)于 IO 操作,注冊(cè)了回調(diào)就立刻返回,同時(shí)每次迭代都會(huì)對(duì)已完成的 IO 執(zhí)行回調(diào),網(wǎng)絡(luò)請(qǐng)求不再阻塞整個(gè)服務(wù)器。
          上面的偽代碼僅便于理解,具體實(shí)現(xiàn)細(xì)節(jié)更復(fù)雜。而且就連接受新請(qǐng)求也是在從操作系統(tǒng)得到監(jiān)聽端口的 IO 事件后進(jìn)行的。
          我們?nèi)绻蜒h(huán)部分還有 call_backs 字典拆分到單獨(dú)模塊,就能得到一個(gè) EventLoop,也就是 Python 標(biāo)準(zhǔn)庫(kù) asyncio 包中提供的 ioloop

          0x02 用生成器消除 callback

          著重看下我們業(yè)務(wù)中經(jīng)常寫的 handler 函數(shù),在有獨(dú)立的 ioloop 后,它現(xiàn)在變成類似這樣:
          def?handler(request):
          ????#?業(yè)務(wù)邏輯代碼...
          ????#?需要執(zhí)行一次?API?請(qǐng)求
          ????def?call_back(result):
          ????????#?使用?API?返回的result完成剩余工作
          ????????print(result)
          ????#?沒(méi)有io_call這個(gè)方法,這里只是示意,表示注冊(cè)一個(gè)IO操作
          ????asyncio.get_event_loop().io_call(api,?call_back)
          到這里,性能問(wèn)題已經(jīng)解決了:我們不再需要多線程就能源源不斷接受新請(qǐng)求,而且不用care依賴的 API 響應(yīng)有多慢。
          但是我們也引入了一個(gè)新問(wèn)題,原來(lái)流暢的業(yè)務(wù)邏輯代碼現(xiàn)在被拆成了兩部分,請(qǐng)求 API 之前的代碼還正常,請(qǐng)求 API 之后的代碼只能寫在回調(diào)函數(shù)里面了。
          這里我們業(yè)務(wù)邏輯只有一個(gè) API 調(diào)用,如果有多個(gè) API ,再加上對(duì) Redis 或者 MySQL 的調(diào)用(它們本質(zhì)也是網(wǎng)絡(luò)請(qǐng)求),整個(gè)邏輯會(huì)被拆分的更散,這對(duì)業(yè)務(wù)開發(fā)是一筆負(fù)擔(dān)。
          對(duì)于有匿名函數(shù)的一些語(yǔ)言(沒(méi)錯(cuò)就是JavaScript),還可能會(huì)引發(fā)所謂的「回調(diào)地獄」。
          接下來(lái)我們想辦法解決這個(gè)問(wèn)題。
          我們很容易會(huì)想到:如果函數(shù)在運(yùn)行到網(wǎng)絡(luò) IO 操作處后能夠暫停,完成后又能在斷點(diǎn)處喚醒就好了。
          如果你對(duì) Python 的「生成器」熟悉,你應(yīng)該會(huì)發(fā)現(xiàn),它恰好具有這個(gè)功能:
          def?example():
          ????value?=?yield?2
          ????print("get",?value)
          ????return?value

          g?=?example()
          #?啟動(dòng)生成器,我們會(huì)得到?2
          got?=?g.send(None)
          print(got)??#?2

          try:
          ????#?再次啟動(dòng)?會(huì)顯示?"get?4",?就是我們傳入的值
          ????got?=?g.send(got*2)
          except?StopIteration?as?e:
          ????#?生成器運(yùn)行完成,將會(huì)print(4),e.value?是生成器return的值
          ????print(e.value)
          函數(shù)中有 yield 關(guān)鍵字,調(diào)用函數(shù)將會(huì)得到一個(gè)生成器,生成器一個(gè)關(guān)鍵的方法 send() 可以跟生成器交互。
          g.send(None) 會(huì)運(yùn)行生成器內(nèi)代碼直到遇到 yield,并返回其后的對(duì)象,也就是 2,生成器代碼就停在這里了,直到我們?cè)俅螆?zhí)行 g.send(got*2),會(huì)把 2*2 也就是 4 賦值給yield 前面的變量 value,然后繼續(xù)運(yùn)行生成器代碼。
          yield 在這里就像一扇門,可以把一件東西從這里送出去,也可以把另一件東西拿進(jìn)來(lái)。
          如果 send 讓生成器運(yùn)行到下一個(gè) yield 前就結(jié)束了,send 調(diào)用會(huì)引發(fā)一個(gè)特殊的異常StopIteration,這個(gè)異常自帶一個(gè)屬性 value,為生成器 return 的值。
          如果我們把我們的 handleryield 關(guān)鍵字轉(zhuǎn)換成一個(gè)生成器,運(yùn)行它來(lái)把 IO 操作的具體內(nèi)容返回,IO 完成后的回調(diào)函數(shù)中把 IO 結(jié)果放回并恢復(fù)生成器運(yùn)行,那就解決了業(yè)務(wù)代碼不流暢的問(wèn)題了:
          def?handler(request):
          ????#?業(yè)務(wù)邏輯代碼...
          ????#?需要執(zhí)行一次?API?請(qǐng)求,直接把?IO?請(qǐng)求信息yield出去
          ????result?=?yield?io_info
          ????#?使用?API?返回的result完成剩余工作
          ????print(result)

          #?這個(gè)函數(shù)注冊(cè)到ioloop中,用來(lái)當(dāng)有新請(qǐng)求的時(shí)候回調(diào)
          def?on_request(request):
          ????#?根據(jù)路由映射獲取到用戶寫的業(yè)務(wù)邏輯函數(shù)
          ????handler?=?get_handler(request)
          ????g?=?handler(request)
          ????#?首次啟動(dòng)獲得io_info
          ????io_info?=?g.send(None)

          ????#?io完成回調(diào)函數(shù)
          ????def?call_back(result):
          ????????#?重新啟動(dòng)生成器
          ????????g.send(result)

          ????asyncio.get_event_loop().io_call(io_info,?call_back)
          上面的例子,用戶寫的 handler 代碼已經(jīng)不會(huì)被打散到 callback 中,on_request 函數(shù)使用 callbackioloop 交互,但它會(huì)被實(shí)現(xiàn)在 Web 框架中,對(duì)用戶不可見。
          上面代碼足以給我們提供用生成器消滅的 callback 的啟發(fā),但局限性有兩點(diǎn):
          1. 業(yè)務(wù)邏輯中僅發(fā)起一次網(wǎng)絡(luò) IO,但實(shí)際中往往更多
          2. 業(yè)務(wù)邏輯沒(méi)有調(diào)用其他異步函數(shù)(協(xié)程),但實(shí)際中我們往往會(huì)調(diào)用其他協(xié)程

          0x03 解決完整調(diào)用鏈

          我們來(lái)看一個(gè)更復(fù)雜的例子:
          其中 request 執(zhí)行真正的 IO,func1func2 僅調(diào)用。顯然我們的代碼只能寫成這樣:
          def?func1():
          ????ret?=?yield?request("http://test.com/foo")
          ????ret?=?yield?func2(ret)
          ????return?ret

          def?func2(data):
          ????result?=?yield?request("http://test.com/"+data)
          ????return?result

          def?request(url):
          ????#?這里模擬返回一個(gè)io操作,包含io操作的所有信息,這里用字符串簡(jiǎn)化代替
          ????result?=?yield?"iojob?of?%s"?%?url
          ????return?result
          對(duì)于 request,我們把 IO 操作通過(guò) yield 暴露給框架。
          對(duì)于 func1func2,調(diào)用 request 顯然也要加 yield 關(guān)鍵字,否則 request 調(diào)用返回一個(gè)生成器后不會(huì)暫停,繼續(xù)執(zhí)行后續(xù)邏輯顯然會(huì)出錯(cuò)。
          這基本就是我們?cè)跊](méi)有 yield fromaysncawait 時(shí)代,在 tornado 框架中寫異步代碼的樣子。
          要運(yùn)行整個(gè)調(diào)用棧,大概流程如下:
          1. 調(diào)用 func1() 得到生成器
          2. 調(diào)用 send(None) 啟動(dòng)它得到會(huì)得到 request("http://test.com/foo") 的結(jié)果,還是生成器對(duì)象
          3. send(None) 啟動(dòng)由 request() 產(chǎn)生的生成器,會(huì)得到 IO 操作,由框架注冊(cè)到 ioloop 并指定回調(diào)
          4. IO 完成后的回調(diào)函數(shù)內(nèi)喚醒 request 生成器,生成器會(huì)走到 return 語(yǔ)句結(jié)束
          5. 捕獲異常得到 request 生成器的返回值,將上一層 func1 喚醒,同時(shí)又得到 func2() 生成器
          6. 繼續(xù)執(zhí)行...
          對(duì)算法和數(shù)據(jù)結(jié)構(gòu)熟悉的朋友遇到這種前進(jìn)后退的遍歷邏輯,可以遞歸也可以用棧,因?yàn)檫f歸使用生成器還做不到,我們可以使用棧,其實(shí)這就是「調(diào)用棧」一詞的由來(lái)。
          借助棧,我們可以把整個(gè)調(diào)用鏈上串聯(lián)的所有生成器對(duì)表現(xiàn)為一個(gè)生成器,對(duì)其不斷 send 就能不斷得到所有 IO 操作信息并推動(dòng)調(diào)用鏈前進(jìn),實(shí)現(xiàn)方法如下:
          1. 第一個(gè)生成器入棧
          2. 調(diào)用 send,如果得到生成器就入棧并進(jìn)入下一輪迭代
          3. 遇到到 IO 請(qǐng)求 yield 出來(lái),讓框架注冊(cè)到 ioloop
          4. IO 操作完成后被喚醒,緩存結(jié)果并出棧,進(jìn)入下一輪迭代,目的讓上層函數(shù)使用 IO 結(jié)果恢復(fù)運(yùn)行
          5. 如果一個(gè)生成器運(yùn)行完畢,也需要和4一樣讓上層函數(shù)恢復(fù)運(yùn)行
          如果實(shí)現(xiàn)出來(lái),代碼不長(zhǎng)但信息量比較大。
          它把整個(gè)調(diào)用鏈對(duì)外變成一個(gè)生成器,對(duì)其調(diào)用 send,就能整個(gè)調(diào)用鏈中的 IO,完成這些 IO,繼續(xù)推動(dòng)調(diào)用鏈內(nèi)的邏輯執(zhí)行,直到整體邏輯結(jié)束:
          def?wrapper(gen):
          ????#?第一層調(diào)用?入棧
          ????stack?=?Stack()
          ????stack.push(gen)

          ????#?開始逐層調(diào)用
          ????while?True:
          ????????#?獲取棧頂元素
          ????????item?=?stack.peak()

          ????????result?=?None
          ????????#?生成器
          ????????if?isgenerator(item):
          ????????????try:
          ????????????????#?嘗試獲取下層調(diào)用并入棧
          ????????????????child?=?item.send(result)
          ????????????????stack.push(child)
          ????????????????#?result?使用過(guò)后就還原為None
          ????????????????result?=?None
          ????????????????#?入棧后直接進(jìn)入下次循環(huán),繼續(xù)向下探索
          ????????????????continue
          ????????????except?StopIteration?as?e:
          ????????????????#?如果自己運(yùn)行結(jié)束了,就暫存result,下一步讓自己出棧
          ????????????????result?=?e.value
          ????????else:??#?IO?操作
          ????????????#?遇到了?IO?操作,yield?出去,IO?完成后會(huì)被用?IO?結(jié)果喚醒并暫存到?result
          ????????????result?=?yield?item

          ????????#?走到這里則本層已經(jīng)執(zhí)行完畢,出棧,下次迭代將是調(diào)用鏈上一層
          ????????stack.pop()
          ????????#?沒(méi)有上一層的話,那整個(gè)調(diào)用鏈都執(zhí)行完成了,return????????
          ????????if?stack.empty():
          ????????????print("finished")
          ????????????return?result
          這可能是最復(fù)雜的部分,如果看起來(lái)吃力的話,其實(shí)只要明白,對(duì)于上面示例中的調(diào)用鏈,它可以實(shí)現(xiàn)的效果如下就好了:
          w?=?wrapper(func1())
          #?將會(huì)得到?"iojob?of?http://test.com/foo"
          w.send(None)
          #?上個(gè)iojob?foo?完成后的結(jié)果"bar"傳入,繼續(xù)運(yùn)行,得到?"iojob?of?http://test.com/bar"
          w.send("bar")
          #?上個(gè)iojob bar 完成后的結(jié)構(gòu)"barz"傳入,繼續(xù)運(yùn)行,結(jié)束。
          w.send("barz")
          有了這部分以后框架再加上配套的代碼:
          #?維護(hù)一個(gè)就緒列表,存放所有完成的IO事件,格式為(wrapper,result)?
          ready?=?[]

          def?on_request(request):
          ????handler?=?get_handler(request)
          ????#?使用?wrapper?包裝后,可以只通過(guò)?send?處理?IO?了
          ????g?=?wrapper(func1())
          ????#?把開始狀態(tài)直接視為結(jié)果為None的就緒狀態(tài)
          ????ready.append((g,?None))

          #?讓ioloop每輪循環(huán)都執(zhí)行此函數(shù),用來(lái)處理的就緒的IO
          def?process_ready(self):
          ????def?call_back(g,?result):
          ????????ready.append((g,?result))?

          ????#?遍歷所有已經(jīng)就緒生成器,將其向下推進(jìn)
          ????for?g,?result?in?self.ready:??
          ????????#?用result喚醒生成器,并得到下一個(gè)io操作
          ????????io_job?=?g.send(result)
          ????????#?注冊(cè)io操作?完成后把生成器加入就緒列表,等待下一輪處理
          ????????asyncio.get_event_loop().io_call(
          ????????????io_job,?lambda?result:?ready.append((g,?result)
          這里核心思想是維護(hù)一個(gè)就緒列表,ioloop 每輪迭代都來(lái)掃一遍,推動(dòng)就緒的狀態(tài)的生成器向下運(yùn)行,并把新的 IO 操作注冊(cè),IO 完成后再次加入就緒,經(jīng)過(guò)幾輪 ioloop 的迭代一個(gè) handler 最終會(huì)被執(zhí)行完成。
          至此,我們使用生成器寫法寫業(yè)務(wù)邏輯已經(jīng)可以正常運(yùn)行。

          0x04 提高擴(kuò)展性

          如果到這里能讀懂, Python 的協(xié)程原理基本就明白了。
          我們已經(jīng)實(shí)現(xiàn)了一個(gè)微型的協(xié)程框架,標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)細(xì)節(jié)跟這里看起來(lái)大不一樣,但具體的思想是一致的。
          我們的協(xié)程框架有一個(gè)限制,我們只能把 IO 操作異步化,雖然在網(wǎng)絡(luò)編程和 Web 編程的世界里,阻塞的基本只有 IO 操作,但也有一些例外,比如我想讓當(dāng)前操作 sleep 幾秒,用 time.sleep() 又會(huì)讓整個(gè)線程阻塞住,就需要特殊實(shí)現(xiàn)。再比如,可以把一些 CPU 密集的操作通過(guò)多線程異步化,讓另一個(gè)線程通知事件已經(jīng)完成后再執(zhí)行后續(xù)。
          所以,協(xié)程最好能與網(wǎng)絡(luò)解耦開,讓等待網(wǎng)絡(luò)IO只是其中一種場(chǎng)景,提高擴(kuò)展性。
          Python 官方的解決方案是讓用戶自己處理阻塞代碼,至于是向 ioloop 來(lái)注冊(cè) IO 事件還是開一個(gè)線程完全由你自己,并提供了一個(gè)標(biāo)準(zhǔn)「占位符」Future,表示他的結(jié)果等到未來(lái)才會(huì)有,其部分原型如下:
          class?Future
          ????#?設(shè)置結(jié)果
          ????def?set_result(result):
          ?pass
          ????#?獲取結(jié)果
          ????def?result():??pass
          ????#??表示這個(gè)future對(duì)象是不是已經(jīng)被設(shè)置過(guò)結(jié)果了
          ????def?done():?pass
          ????#?設(shè)置在他被設(shè)置結(jié)果時(shí)應(yīng)該執(zhí)行的回調(diào)函數(shù),可以設(shè)置多個(gè)
          ????def?add_done_callback(callback):??pass
          我們的稍加改動(dòng)就能支持 Future,讓擴(kuò)展性變得更強(qiáng)。對(duì)于用戶代碼的中的網(wǎng)絡(luò)請(qǐng)求函數(shù) request
          #?現(xiàn)在?request?函數(shù),不是生成器,它返回future
          def?request(url):
          ????#?future?理解為占位符
          ????fut?=?Future()

          ????def?callback(result):
          ????????#?當(dāng)網(wǎng)絡(luò)IO完成回調(diào)的時(shí)候給占位符賦值
          ????????fut.set_result(result)
          ????asyncio.get_event_loop().io_call(url,?callback)

          ????#?返回占位符
          ????return?future
          現(xiàn)在,request 不再是一個(gè)生成器,而是直接返回 future
          而對(duì)于位于框架中處理就緒列表的函數(shù):
          def?process_ready(self):
          ????def?callback(fut):
          ????????#?future被設(shè)置結(jié)果會(huì)被放入就緒列表
          ????????ready.append((g,?fut.result()))

          ????#?遍歷所有已經(jīng)就緒生成器,將其向下推進(jìn)
          ????for?g,?result?in?self.ready:??
          ????????#?用result喚醒生成器,得到的不再是io操作,而是future
          ????????fut?=?g.send(result)
          ????????#?future被設(shè)置結(jié)果的時(shí)候會(huì)調(diào)用callback
          ????????fut.add_done_callback(callback)

          0x05 發(fā)展和變革

          許多年前用 tornado 的時(shí)候,大概只有一個(gè) yield 關(guān)鍵字可用,協(xié)程要想實(shí)現(xiàn),就是這么個(gè)思路,甚至 yield 關(guān)鍵字和 return 關(guān)鍵字不能一個(gè)函數(shù)里面出現(xiàn),你要想在生成器運(yùn)行完后返回一個(gè)值,需要手動(dòng) raise 一個(gè)異常,雖然效果跟現(xiàn)在 return 一樣,但寫起來(lái)還是很別扭,不優(yōu)雅。
          后來(lái)有了 yield from 表達(dá)式。它可以做什么?
          通俗地說(shuō),它就是做了上面那個(gè)生成器 wrapper 所做的事:通過(guò)棧實(shí)現(xiàn)調(diào)用鏈遍歷的 ,它是 wrapper 邏輯的語(yǔ)法糖。
          有了它,同一個(gè)例子你可以這么寫:
          def?func1():
          ????#?注意?yield?from
          ????ret?=?yield?from?request("http://test.com/foo")
          ????#?注意?yield?from
          ????ret?=?yield?from?func2(ret)?
          ????return?ret

          def?func2(data):
          ????#?注意?yield?from
          ????result?=?yield?from?request("http://test.com/"+data)
          ????return?result

          #?現(xiàn)在?request?函數(shù),不是生成器,它返回future
          def?request(url):
          ????#?同上基于future實(shí)現(xiàn)的request
          然后你就不再需要那個(gè)燒腦的 wrapper 函數(shù)了:
          g?=?func1()
          #?返回第一個(gè)請(qǐng)求的?future?
          g.send(None)
          #?繼續(xù)運(yùn)行,自動(dòng)進(jìn)入func2?并得到第它里面的那個(gè)future
          g.send("bar")
          #?繼續(xù)運(yùn)行,完成調(diào)用鏈?zhǔn)S噙壿嫞瑨伋鯯topIteration異常
          g.send("barz")
          yield from 直接打通了整個(gè)調(diào)用鏈,已經(jīng)是很大的進(jìn)步了,但是用來(lái)異步編程看著還是別扭,其他語(yǔ)言都有專門的協(xié)程 asyncawait 關(guān)鍵字了,直到再后來(lái)的版本把這些內(nèi)容用專用的 asyncawait 關(guān)鍵字包裝,才成為今天比較優(yōu)雅的樣子。

          0x06 總結(jié)和比較

          總的來(lái)說(shuō), Python 的原生的協(xié)程從兩方面實(shí)現(xiàn):
          1. 基于 IO 多路復(fù)用技術(shù),讓整個(gè)應(yīng)用在 IO 上非阻塞,實(shí)現(xiàn)高效率
          2. 通過(guò)生成器讓分散的 callback 代碼變成同步代碼,減少業(yè)務(wù)編寫困難
          有生成器這種對(duì)象的語(yǔ)言,其 IO 協(xié)程實(shí)現(xiàn)大抵如此,JavaScript 協(xié)程的演進(jìn)基本一模一樣,關(guān)鍵字相同,Future 類比 Promise 本質(zhì)相同。
          但是對(duì)于以協(xié)程出名的 Go 的協(xié)程實(shí)現(xiàn)跟這個(gè)就不同了,并不顯式地基于生成器。
          如果類比的話,可以 Python 的 gevent 算作一類,都是自己實(shí)現(xiàn) runtime,并 patch 掉系統(tǒng)調(diào)用接入自己的 runtime,自己來(lái)調(diào)度協(xié)程,gevent 專注于網(wǎng)絡(luò)相關(guān),基于網(wǎng)絡(luò) IO 調(diào)度,比較簡(jiǎn)單,而 Go 實(shí)現(xiàn)了完善的多核支持,調(diào)度更加復(fù)雜和完善,而且創(chuàng)造了基于 channel 新編程范式。
          來(lái)源:https://zhuanlan.zhihu.com/p/330549526
          作者:毛豆花生


          瀏覽 26
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕无码不卡免费视频 | 天天干天天撸 | 人人看人人舔 | 骚逼特写| 五月天激情导航 |