1. 程式人生 > >深入理解協程(二):yield from實現非同步協程

深入理解協程(二):yield from實現非同步協程

原創不易,轉載請聯絡作者

深入理解協程分為三部分進行講解:

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

本篇為深入理解協程系列文章的第二篇。

yield from

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

第一個功能:讓巢狀生成器不必再通過迴圈迭代yield,而可以直接使用yield from

看一段程式碼:

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減少了一次迴圈。

第二個功能:開啟雙向通道,把最外層給呼叫方與最內層的子生成器連結起來,二者可以直接通訊。

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

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

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

說明:

解釋1:g2是呼叫generator_2()得到的生成器物件,作為協程使用。

解釋2:預啟用協程g2

解釋3:generator_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。

相關推薦

深入理解yield from實現非同步

原創不易,轉載請聯絡作者 深入理解協程分為三部分進行講解: 協程的引入 yield from實現非同步協程 async/await實現非同步協程 本篇為深入理解協程系列文章的第二篇。 yield from yield from是Python3.3(PEP 380)引入的新語法。主要用於解決在生成器

深入理解async/await實現非同步

原創不易,轉載請聯絡作者 深入理解協程分為三部分進行講解: 協程的引入 yield from實現非同步協程 async/await實現非同步協程 本篇為深入理解協程系列文章的最後一篇。 從本篇你將瞭解到: async/await的使用。 如何從yield from風格的協程修改為async/aw

Java——深入理解Class物件Class物件的載入及其獲取方式

上一篇部落格Java——深入理解Class物件(一)帶大家簡單認識了一下Java中Class物件。 現在帶大家瞭解一下Class物件的載入及其獲取方式。 1.Class物件的載入 在Java——深入理解Class物件(一)我們已提到過,Class物件是由JVM載入的,那它必然不會是胡亂載

Javascript面向對象編構造函數的繼承

沒有 cal type 這一 今天 nts 實現繼承 刪除 函數綁定 今天要介紹的是,對象之間的"繼承"的五種方法。 比如,現在有一個"動物"對象的構造函數。   function Animal(){     this.species = "動物";   } 還有一個

深入理解JavaScript系列16閉包Closures

ava hive auto flow style this quest 情況 知識 介紹 本章我們將介紹在JavaScript裏大家常常來討論的話題 —— 閉包(closure)。閉包事實上大家都已經談爛了。雖然如此,這裏還是要試著從理論角度來討論下閉包,

並發編

imu 並發數 提交 任務調度 core info 回收 log 減少 相比於線程池,我們可能接觸new Thread更多一點,既然有了new Thread我們為什麽還要使用線程池呢? new Thread的弊端 a、每次new Thread新建對象,

深入理解設計模式12職責鏈模式

一、什麼是職責鏈模式 客戶端發出一個請求,鏈上的物件都有機會來處理這一請求,而客戶端不需要知道誰是具體的處理物件。這樣就實現了請求者和接受者之間的解耦,並且在客戶端可以實現動態的組合職責鏈。使程式設計更有靈活性。 定義:使多個物件都有機會處理請求,從而避免了請求的傳送者和接受者之間的耦合關係。將這些物件連

深入理解JavaScript系列5強大的原型和原型鏈

JavaScript 不包含傳統的類繼承模型,而是使用 prototypal 原型模型。 雖然這經常被當作是 JavaScript 的缺點被提及,其實基於原型的繼承模型比傳統的類繼承還要強大。實現傳統的類繼承模型是很簡單,但是實現 JavaScript 中的原型繼承則要困難的多。 &l

深入理解JavaScript系列2揭祕命名函式表示式 命名函式表示式 函式表示式 函式宣告

還有一種函式表示式不太常見,就是被括號括住的(function foo(){}),他是表示式的原因是因為括號 ()是一個分組操作符,它的內部只能包含表示式,我們來看幾個例子: 函式宣告只能出現在程式或函式體內。 如果function foo(){}是作為賦值表示式的一部分的

深入理解設計模式13直譯器模式

一、什麼是直譯器模式 定義:給定一個語言,定義一個文法的一種表示, 並定義一個直譯器, 這個直譯器使用該表示來解釋語言中的句子。   直譯器模式所涉及的角色如下所示:   (1)抽象表示式(Expression)角色:宣告一個所有的具體表達式角色都需要實現的抽象介面。這個介面主要是一個i

Java——深入理解Class物件什麼是Class物件

Class類是我們再熟悉不過的東西,但是對於Class物件,很多人卻是一臉懵逼。 Class物件到底是什麼呢?今天我們就來深入瞭解一下它。 1.RTTI的概念 RTTI(Run-Time Type Identification),即執行時型別識別,這個詞一直是 C++ 中的概念,至

深入理解 Laravel Eloquent——模型間關係關聯

Eloquent是什麼 Eloquent 是一個 ORM,全稱為 Object Relational Mapping,翻譯為 “物件關係對映”(如果只把它當成 Database Abstraction Layer 陣列庫抽象層那就太小看它了)。所謂 “物件”,就是本文所說的 “模型(Model)

深入理解JavaScript系列2揭祕命名函式表示式

還有一種函式表示式不太常見,就是被括號括住的(function foo(){}),他是表示式的原因是因為括號 ()是一個分組操作符,它的內部只能包含表示式,我們來看幾個例子: 函式宣告只能出現在程式或函式體內。 如果function foo(

深入理解設計模式15訪問者模式

一、什麼是訪問者模式 定義:表示一個作用於其物件結構中的各元素的操作,它使你可以在不改變各元素類的前提下定義作用於這些元素的新操作。 可以對定義這麼理解:有這麼一個操作,它是作用於一些元素之上的,而這些元素屬於某一個物件結構。同時這個操作是在不改變各元素類的前提下,在這個前提下定義新操作是訪問者模式精髓中

深入理解JavaScript系列4立即呼叫的函式表示式

javascript 函式function前面的一元操作符, 感嘆號、小括號、一元操作符!()+-|| 看下面內容之前可以先看看上面的文章,總結的非常贊 前言 大家學JavaScript的時候,經常遇到自執行匿名函式的程式碼,今天我們主要就來想想說一下自執行 在詳細

深入理解HTTP協議——協議詳解篇

1.HTTP/1.0和HTTP/1.1的比較 RFC 1945定義了HTTP/1.0版本,RFC 2616定義了HTTP/1.1版本。 1.1建立連線方面 HTTP/1.0 每次請求都需要建立新的TCP連線,連線不能複用。HTTP/1.1 新的請求可以在上次請求建立

深入理解JavaScript系列15函式Functions

  詳情請檢視:https://www.cnblogs.com/TomXu/archive/2012/01/30/2326372.html   本章節我們要著重介紹的是一個非常常見的ECMAScript物件——函式(function),我們將詳細講解一下各種型別的函式

深入理解Docker Volume

轉自:http://dockone.io/article/129 【編者的話】繼上一篇文章深入理解Docker Volume(一)後,DockerOne翻譯了深入理解Volume的第二篇文章。本文重點介紹了兩種建立Volume方式的異同以及使用docker run命令建立

深入理解計算機系統筆記連結

理解連結有很多好處: 有助於構造大型程式有助於避免一些危險程式設計錯誤有助於理解其他重要的系統概念讓你能夠利用共享庫1. 編譯器驅動程式 編譯命令,假設有main.c和swap.c兩個原始檔 $ gcc -O2 -g -o p main.c swap.c 實際上編譯過程

深入理解阻塞佇列——ArrayBlockingQueue原始碼分析

在深入理解阻塞佇列(一)——基本結構中,介紹了BlockingQueue這一介面的子類以及子介面。本文主要就其中的一個實現類:ArrayBlockingQueue進行原始碼分析,分析阻塞佇列的阻塞是如何實現的。 概述 ArrayBlockingQueue