手把手教你如何使用Python的非同步IO框架:asyncio(上)

Python程式設計時光發表於2018-07-04

作者:MING
個人公眾號:Python程式設計時光
個人微信:mrbensonwon

注:本系列已在微信公眾號更新完成。檢視最新文章,請關注公眾號獲取。

大家好,併發程式設計 進入第九篇。

通過前兩節的鋪墊(關於協程的使用),今天我們終於可以來介紹我們整個系列的重點 -- asyncio

asyncio是Python 3.4版本引入的標準庫,直接內建了對非同步IO的支援。

有些同學,可能很疑惑,既然有了以生成器為基礎的協程,我們直接使用yieldyield from 不就可以手動實現對IO的排程了嗎? 為何Python吃飽了沒事幹,老重複造輪子。

這個問題很好回答,就跟為什麼會有Django,為什麼會有Scrapy,是一個道理。

他們都是框架,將很多很重複性高,複雜度高的工作,提前給你做好,這樣你就可以專注於業務程式碼的研發。

跟著小明學完了協程的那些個難點,你是不是也發現了,協程的知識點我已經掌握了,但是我還是不知道怎麼用,如何使用,都說它可以實現併發,但是我還是不知道如何入手?

那是因為,我們現在還缺少一個成熟的框架,幫助你完成那些複雜的動作。這個時候,ayncio就這麼應運而生了。

如何定義/建立協程

還記得在前兩章節的時候,我們建立了生成器,是如何去檢驗我們建立的是不是生成器物件嗎?

我們是藉助了isinstance()函式,來判斷是否是collections.abc 裡的Generator類的子類實現的。

同樣的方法,我們也可以用在這裡。

只要在一個函式前面加上 async 關鍵字,這個函式物件是一個協程,通過isinstance函式,它確實是Coroutine型別。

from collections.abc import Coroutine

async def hello(name):
    print('Hello,', name)

if __name__ == '__main__':
    # 生成協程物件,並不會執行函式內的程式碼
    coroutine = hello("World")

    # 檢查是否是協程 Coroutine 型別
    print(isinstance(coroutine, Coroutine))  # True
複製程式碼

前兩節,我們說,生成器是協程的基礎,那我們是不是有辦法,將一個生成器,直接變成協程使用呢。答案是有的。

import asyncio
from collections.abc import Generator, Coroutine

'''
只要在一個生成器函式頭部用上 @asyncio.coroutine 裝飾器
就能將這個函式物件,【標記】為協程物件。注意這裡是【標記】,劃重點。
實際上,它的本質還是一個生成器。
標記後,它實際上已經可以當成協程使用。後面會介紹。
'''



@asyncio.coroutine
def hello():
    # 非同步呼叫asyncio.sleep(1):
    yield from asyncio.sleep(1)


if __name__ == '__main__':
    coroutine = hello()
    print(isinstance(coroutine, Generator))  # True
    print(isinstance(coroutine, Coroutine))  # False
複製程式碼

asyncio的幾個概念

在瞭解asyncio的使用方法前,首先有必要先介紹一下,這幾個貫穿始終的概念。

  • event_loop 事件迴圈:程式開啟一個無限的迴圈,程式設計師會把一些函式(協程)註冊到事件迴圈上。當滿足事件發生的時候,呼叫相應的協程函式。
  • coroutine 協程:協程物件,指一個使用async關鍵字定義的函式,它的呼叫不會立即執行函式,而是會返回一個協程物件。協程物件需要註冊到事件迴圈,由事件迴圈呼叫。
  • future 物件: 代表將來執行或沒有執行的任務的結果。它和task上沒有本質的區別
  • task 任務:一個協程物件就是一個原生可以掛起的函式,任務則是對協程進一步封裝,其中包含任務的各種狀態。Task 物件是 Future 的子類,它將 coroutine 和 Future 聯絡在一起,將 coroutine 封裝成一個 Future 物件。
  • async/await 關鍵字:python3.5 用於定義協程的關鍵字,async定義一個協程,await用於掛起阻塞的非同步呼叫介面。其作用在一定程度上類似於yield。

這幾個概念,幹看可能很難以理解,沒事,往下看例項,然後再回來,我相信你一定能夠理解。

學習協程是如何工作的

協程完整的工作流程是這樣的

  • 定義/建立協程物件
  • 將協程轉為task任務
  • 定義事件迴圈物件容器
  • 將task任務扔進事件迴圈物件中觸發

光說不練假把戲,一起來看下

import asyncio

async def hello(name):
    print('Hello,', name)

# 定義協程物件
coroutine = hello("World")

# 定義事件迴圈物件容器
loop = asyncio.get_event_loop()
# task = asyncio.ensure_future(coroutine)

# 將協程轉為task任務
task = loop.create_task(coroutine)

# 將task任務扔進事件迴圈物件中並觸發
loop.run_until_complete(task)
複製程式碼

輸出結果,當然顯而易見

Hello, World
複製程式碼

await與yield對比

前面我們說,await用於掛起阻塞的非同步呼叫介面。其作用在一定程度上類似於yield。

注意這裡是,一定程度上,意思是效果上一樣(都能實現暫停的效果),但是功能上卻不相容。就是你不能在生成器中使用await,也不能在async 定義的協程中使用yield

小明不是胡說八道的。有實錘。

普通函式中 不能使用 await
普通函式中 不能使用 await

再來一錘。

async 中 不能使用yield
async 中 不能使用yield

除此之外呢,還有一點很重要的。

  • yield from 後面可接 可迭代物件,也可接future物件/協程物件;
  • await 後面必須要接 future物件/協程物件

如何驗證呢?

yield from 後面可接 可迭代物件,這個前兩章已經說過了,這裡不再贅述。
接下來,就只要驗證,yield fromawait都可以接future物件/協程物件就可以了。

驗證之前呢,要先介紹一下這個函式:
asyncio.sleep(n),這貨是asyncio自帶的工具函式,他可以模擬IO阻塞,他返回的是一個協程物件。

func = asyncio.sleep(2)
print(isinstance(func, Future))      # False
print(isinstance(func, Coroutine))   # True
複製程式碼

還有,要學習如何建立Future物件,不然怎麼驗證。
前面概念裡說過,Task是Future的子類,這麼說,我們只要建立一個task物件即可。

import asyncio
from asyncio.futures import Future

async def hello(name):
    await asyncio.sleep(2)
    print('Hello, ', name)

coroutine = hello("World")

# 將協程轉為task物件
task = asyncio.ensure_future(coroutine)

print(isinstance(task, Future))   # True
複製程式碼

好了,接下來,開始驗證。

驗證通過
驗證通過

繫結回撥函式

非同步IO的實現原理,就是在IO高的地方掛起,等IO結束後,再繼續執行。在絕大部分時候,我們後續的程式碼的執行是需要依賴IO的返回值的,這就要用到回撥了。

回撥的實現,有兩種,一種是絕大部分程式設計師喜歡的,利用的同步程式設計實現的回撥。
這就要求我們要能夠有辦法取得協程的await的返回值。

import asyncio
import time

async def _sleep(x):
    time.sleep(2)
    return '暫停了{}秒!'.format(x)


coroutine = _sleep(2)
loop = asyncio.get_event_loop()

task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)

# task.result() 可以取得返回結果
print('返回結果:{}'.format(task.result()))
複製程式碼

輸出

返回結果:暫停了2秒!
複製程式碼

還有一種是通過asyncio自帶的新增回撥函式功能來實現。

import time
import asyncio


async def _sleep(x):
    time.sleep(2)
    return '暫停了{}秒!'.format(x)

def callback(future):
    print('這裡是回撥函式,獲取返回結果是:', future.result())

coroutine = _sleep(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)

# 新增回撥函式
task.add_done_callback(callback)

loop.run_until_complete(task)
複製程式碼

輸出

這裡是回撥函式,獲取返回結果是: 暫停了2秒!
複製程式碼

和上面的結果是一樣的。


關注公眾號,獲取最新文章
關注公眾號,獲取最新文章

相關文章