深刻理解協程(二):yield from實現異步協程

原創不易,轉載請聯繫做者python

深刻理解協程分爲三部分進行講解:併發

  • 協程的引入
  • yield from實現異步協程
  • async/await實現異步協程

本篇爲深刻理解協程系列文章的第二篇。框架

yield from

yield from是Python3.3(PEP 380)引入的新語法。主要用於解決在生成器中不方便使用生成器的問題。主要有兩個功能。異步

第一個功能:讓嵌套生成器沒必要再經過循環迭代yield,而能夠直接使用yield fromasync

看一段代碼:函數

titles = ['Python', 'Java', 'C++']
def func1(titles):
    yield titles

def func2(titles):
    yield from titles

for title in func1(titles):
    print(title)

for title in func2(titles):
    print(title)
    
# 輸出結果
['Python', 'Java', 'C++']
Python
Java
C++

yield返回的完整的titles列表,而yield from返回的是列表中的具體元素。yield from能夠看做是for title in titles: yield title的縮寫。這樣就能夠用yield from減小了一次循環。oop

第二個功能:打開雙向通道,把最外層給調用方與最內層的子生成器連接起來,兩者能夠直接通訊。.net

第二個功能聽起來就讓人頭大。咱們再舉一個例子進行說明:線程

【舉個例子】:經過生成器實現整數相加,經過send()函數想生成器中傳入要相加的數字,最後傳入None結束相加。total保存結果。code

def generator_1():      # 子生成器
    total = 0
    while True:
        x = yield       # 解釋4
        print(f'+ {x}')
        if not x:
            break
        total += x
    return total        # 解釋5

def generator_2():      # 委託生成器
    while True:
        total = yield from generator_1()    # 解釋3
        print(f'total: {total}')

if __name__ == '__main__':      # 調用方
    g2 = generator_2()      # 解釋1
    g2.send(None)           # 解釋2
    g2.send(2)              # 解釋6
    g2.send(3)              
    g2.send(None)           # 解釋7

# 輸出結果
+ 2
+ 3
+ None
total: 5

說明

解釋1g2是調用generator_2()獲得的生成器對象,做爲協程使用。

解釋2:預激活協程g2

解釋3generator_2接收的值都會通過yield from處理,經過管道傳入generator_1實例。generator_2會在yield from處暫停,等待generator_1實例傳回的值賦值給total

解釋4:調用方傳入的值都會傳到這裏。

解釋5:此處返回的total正是generator_2()中解釋3處等待返回的值。

解釋6:傳入2進行計算。

解釋7:在計算的結尾傳入None,跳出generator_1()的循環,結束計算。

說到這裏,相信看過《深刻理解協程(一):協程的引入》的朋友應該就容易理解上面這段代碼的運行流程了。

藉助上面例子,說明一下隨yield from一塊兒引入的3個概念:

  • 子生成器

    yield from獲取任務並完成具體實現的生成器。

  • 委派生成器

    包含有 yield from表達式的生成器函數。負責給子生成器委派任務。

  • 調用方

    指調用委派生成器的客戶端代碼。

在每次調用send(value)時,value不是傳遞給委派生成器,而是藉助yield fromvalue傳遞給了子生成器的yield

結合asyncio實現異步協程

asyncio是Python 3.4 試驗性引入的異步I/O框架(PEP 3156),提供了基於協程作異步I/O編寫單線程併發代碼的基礎設施。其核心組件有事件循環(Event Loop)、協程(Coroutine)、任務(Task)、將來對象(Future)以及其餘一些擴充和輔助性質的模塊。

在引入asyncio的時候,還提供了一個裝飾器@asyncio.coroutine用於裝飾使用了yield from的函數,以標記其爲協程。

在實現異步協程以前,咱們先看一個同步的案例:

import time
def taskIO_1():
    print('開始運行IO任務1...')
    time.sleep(2)  # 假設該任務耗時2s
    print('IO任務1已完成,耗時2s')
def taskIO_2():
    print('開始運行IO任務2...')
    time.sleep(3)  # 假設該任務耗時3s
    print('IO任務2已完成,耗時3s')

start = time.time()
taskIO_1()
taskIO_2()
print('全部IO任務總耗時%.5f秒' % float(time.time()-start))
# 輸出結果
開始運行IO任務1...
IO任務1已完成,耗時2s
開始運行IO任務2...
IO任務2已完成,耗時3s
全部IO任務總耗時5.00094秒

能夠看到,使用同步的方式實現多個IO任務的時間是分別執行這兩個IO任務時間的總和。

下面咱們使用yield fromasyncio將上面的同步代碼改爲異步的。修改結果以下:

import time
import asyncio

@asyncio.coroutine # 解釋1
def taskIO_1():
    print('開始運行IO任務1...')
    yield from asyncio.sleep(2)  # 解釋2
    print('IO任務1已完成,耗時2s')
    return taskIO_1.__name__

@asyncio.coroutine 
def taskIO_2():
    print('開始運行IO任務2...')
    yield from asyncio.sleep(3)  # 假設該任務耗時3s
    print('IO任務2已完成,耗時3s')
    return taskIO_2.__name__

@asyncio.coroutine 
def main(): # 調用方
    tasks = [taskIO_1(), taskIO_2()]  # 把全部任務添加到task中
    done,pending = yield from asyncio.wait(tasks) # 子生成器
    for r in done: # done和pending都是一個任務,因此返回結果須要逐個調用result()
        print('協程無序返回值:'+r.result())

if __name__ == '__main__':
    start = time.time()
    loop = asyncio.get_event_loop() # 建立一個事件循環對象loop
    try:
        loop.run_until_complete(main()) # 完成事件循環,直到最後一個任務結束
    finally:
        loop.close() # 結束事件循環
    print('全部IO任務總耗時%.5f秒' % float(time.time()-start))
    
# 輸出結果
開始運行IO任務2...
開始運行IO任務1...
IO任務1已完成,耗時2s
IO任務2已完成,耗時3s
協程無序返回值:taskIO_1
協程無序返回值:taskIO_2
全部IO任務總耗時3.00303秒

說明

解釋1@asyncio.coroutine裝飾器是協程函數的標誌,咱們須要在每個任務函數前加這個裝飾器,並在函數中使用yield from

解釋2:此處假設該任務運行須要2秒,此處使用異步等待2秒asyncio.sleep(2),而非同步等待time.sleep(2)

執行過程

  1. 先經過get_event_loop()獲取了一個標準事件循環loop(由於是一個,因此協程是單線程)
  2. 而後,咱們經過run_until_complete(main())來運行協程(此處把調用方協程main()做爲參數,調用方負責調用其餘委託生成器),run_until_complete的特色就像該函數的名字,直到循環事件的全部事件都處理完才能完整結束.
  3. 進入調用方協程,咱們把多個任務[taskIO_1()和taskIO_2()]放到一個task列表中,可理解爲打包任務。
  4. 咱們使用asyncio.wait(tasks)來獲取一個awaitable objects便可等待對象的集合,經過yield from返回一個包含(done, pending)的元組,done表示已完成的任務列表,pending表示未完成的任務列表。
  5. 由於done裏面有咱們須要的返回結果,但它目前仍是個任務列表,因此要取出返回的結果值,咱們遍歷它並逐個調用result()取出結果便可。
  6. 最後咱們經過loop.close()關閉事件循環。

可見,經過使用協程,極大提升了多任務的執行效率,程序最後消耗的時間是任務隊列中耗時最多時間任務的時長。

總結

本篇講述了:

  • yield from如何實現協程
  • 如何結合asyncio實現異步協程

雖然有了yield from的存在,讓協程實現比以前容易了,可是這種異步協程的實現方式,並非很pythonic。如今已經不推薦使用了。下篇將與您分享更加完善的Python異步實現方式——async/await實現異步協程

參考

Python異步IO之協程(一):從yield from到async的使用

關注公衆號西加加先生一塊兒玩轉Python
在這裏插入圖片描述

相關文章
相關標籤/搜索