python非同步IO程式設計(一)

發表於2019-07-11

python非同步IO程式設計(一)

基礎概念

協程:python  generator與coroutine

非同步IO (async IO):一種由多種語言實現的與語言無關的範例(或模型)。

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

 

非同步IO

執行緒,多執行緒

多執行緒善於處理I/O密集型任務。
多程式擅長處理計算密集型(CPU-bound)任務:強密集迴圈和數學計算都屬於此類。
併發是並行的一種特殊型別(或者說子類),多執行緒是併發的表現形式,多程式是並行的表現形式。
Python通過它的包 multiprocessing,threading 和 concurrent.futures 已經對這兩種形式都提供了長期的支援。

 

非同步IO

非同步IO是一種單程式、單執行緒的設計:它使用協同多工處理機制,是以協程為核心的一種程式設計模型。
非同步IO並不是新發明的概念,它已經存在或正在被構建到其他語言及執行時環境中,如 Go,C# 和 Scala 等。
非同步IO模型非同步IO採用訊息迴圈的模式,在訊息迴圈中,主執行緒不斷地重複“讀取訊息-處理訊息”這一過程:

loop = get_event_loop() //例項訊息佇列
while True:
event = loop.get_event() //從佇列中讀取訊息
process_event(event)    //處理訊息訊息模型其實早在應用在桌面應用程式中了。一個GUI程式的主執行緒就負責不停地讀取訊息並處理訊息。所有的鍵盤、滑鼠等訊息都被髮送到GUI程式的訊息佇列中,然後由GUI程式的主執行緒處理。

由於GUI執行緒處理鍵盤、滑鼠等訊息的速度非常快,所以使用者感覺不到延遲。某些時候,GUI執行緒在一個訊息處理的過程中遇到問題導致一次訊息處理時間過長,此時,使用者會感覺到整個GUI程式停止響應了,敲鍵盤、點滑鼠都沒有反應。
這種情況說明在訊息模型中,處理一個訊息必須非常迅速,否則,主執行緒將無法及時處理訊息佇列中的其他訊息,導致程式看上去停止響應。
訊息模型是如何解決同步IO必須等待IO操作這一問題的呢?當遇到IO操作時,程式碼只負責發出IO請求,不等待IO結果,然後直接結束本輪訊息處理,進入下一輪訊息處理過程。當IO操作完成後,將收到一條“IO完成”的訊息,處理該訊息時就可以直接獲取IO操作結果。
在“發出IO請求”到收到“IO完成”的這段時間裡,同步IO模型下,主執行緒只能掛起,但非同步IO模型下,主執行緒並沒有休息,而是在訊息迴圈中繼續處理其他訊息。這樣,在非同步IO模型下,一個執行緒就可以同時處理多個IO請求,並且沒有切換執行緒的操作。對於大多數IO密集型的應用程式,使用非同步IO將大大提升系統的多工處理能力。

 

 

asyncio

async與asyncio.coroutine

在 python3.5 中,建立一個協程僅僅只需使用 async 關鍵字,而python3.4使用 @asyncio.coroutine 裝飾器。都引入了原生協程或者說非同步生成器。下面的任一程式碼,都可以作為協程工作,形式上也是等同的:

import asyncio

async def ping_server(ip): # 3.5
pass

@asyncio.coroutine
def load_file(path): # 3.4
pass

 

 

yield from與await

3.1 中協程操作只是簡單的生成器呼叫,常見的我們還需要在生成器或者說協程之間相互呼叫,用到yield from。yield from 用於一個generator呼叫另一個generator,主要是為了generator之間的呼叫。
yield from 表示式的使用方式如下:

import asyncio
@asyncio.coroutine
def get_jason(client, url):
file_content = yield from load_file('/Usrs/scott/data.txt')

 

協程的一個關鍵特性是它們可以被連結到一起。(記住,一個協程是可等待的,所以另一個協程可以使用 await 來等待它。)await 將控制器傳遞給時間迴圈。(掛起當前執行的協程與yield from類似),使用方式如下:

async def ping_local(ip):
  return await ping_server('192.168.1.1')

Python3.5 對這兩種呼叫協程的方法都提供了支援,但是推薦 async/await 作為首選。


Python執行的時候, g() 函式範圍內如果遇到表示式 await f(),就是 await 在告訴事件迴圈“掛起 g() 函式,直到 f() 返回結果,在此期間,可以執行其他函式。”

async def g():
#暫停,直到 f()結束再回到g()
  r = await f()
  return r

當你使用 await f() 時,要求 f() 是一個可等待的物件。但這並沒有什麼用。現在,只需要知道可等待物件要麼是(1)其他的協程,要麼就是(2)定義了 .await() 函式且返回迭代器的物件。如果你正在編寫程式,絕大多數情況只需要關注案例#1。

使用規則

1. 使用 await 與 return 的組合建立協程函式。想要呼叫一個協程函式,必須使用 await 等待返回結果。
2. 在 async def 程式碼塊中使用 yield 的情況並不多見(只有Python的近期版本才可用)。當你使用 async for 進行迭代的時候,會建立一個非同步生成器。暫時先忘掉非同步生成器,將目光放在使用 await 與 return 的組合建立協程函式的語法上。
3. 在任何使用 async def 定義的地方都不可以使用 yield from,這會引發異常 SyntaxError。
4. 一如在 def 定義的函式之外使用 yield 會引發異常 SyntaxError,在 async def 定義的協程之外使用 await 也會引發異常 SyntaxError。你只能在協程內部使用 await。

 

 

Event Loop

asyncio的程式設計模型就是一個訊息迴圈。我們從asyncio模組中直接獲取一個EventLoop的引用,然後把需要執行的協程扔到EventLoop中執行,就實現了非同步IO。

用asyncio實現Hello world程式碼如下:

import asyncio

@asyncio.coroutine
def hello():
  print("Hello world!")
# 非同步呼叫asyncio.sleep(1):
  r = yield from asyncio.sleep(1)
  print("Hello again!")

# 獲取EventLoop:
loop = asyncio.get_event_loop()
# 執行coroutine
loop.run_until_complete(hello())
loop.close()

@asyncio.coroutine把一個generator標記為coroutine型別,然後,我們就把這個coroutine扔到EventLoop中執行。
hello()會首先列印出Hello world!,然後,yield from語法可以讓我們方便地呼叫另一個generator。由於asyncio.sleep()也是一個coroutine,所以執行緒不會等待asyncio.sleep(),而是直接中斷並執行下一個訊息迴圈。當asyncio.sleep()返回時,執行緒就可以從yield from拿到返回值(此處是None),然後接著執行下一行語句。
把asyncio.sleep(1)看成是一個耗時1秒的IO操作,在此期間,主執行緒並未等待,而是去執行EventLoop中其他可以執行的coroutine了,因此可以實現併發執行。

async/await版本:

import asyncio

async def hello():
print("Hello World")
r = await asyncio.sleep(1)
print("Again")

loop = asyncio.get_event_loop()
loop.run_until_complete(hello())
loop.close()

我們用Task封裝兩個coroutine試試:

import threading
import asyncio

@asyncio.coroutine
def hello():
  print('Hello world! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
  print('Hello again! (%s)' % threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

觀察執行過程:
Hello world! (<_MainThread(MainThread, started 140735195337472)>)
Hello world! (<_MainThread(MainThread, started 140735195337472)>)
(暫停約1秒)
Hello again! (<_MainThread(MainThread, started 140735195337472)>)
Hello again! (<_MainThread(MainThread, started 140735195337472)>)

由列印的當前執行緒名稱可以看出,兩個coroutine是由同一個執行緒併發執行的。
如果把asyncio.sleep()換成真正的IO操作,則多個coroutine就可以由一個執行緒併發執行。

 

 

參考:

https://mp.weixin.qq.com/s/fJaXmfHfYEk6XL2y8NmKmQ

https://www.jianshu.com/p/2dfaacdd0a90

 

相關文章