小議Python3的原生協程機制

網易雲社群發表於2018-11-02

此文已由作者張耕源授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。


在最近釋出的 Python 3.5 版本中,官方正式引入了 async/await關鍵字、在 asyncio [1] 標準庫中實現了IO多路複用、原生協程(coroutine)與 事件迴圈(event loop),讓人耳目一新,本文也嘗試對 Python 3.5 新增加的原生協程 機制與asyncio標準庫相關的內容做一個小結。

IO多路複用與協程的引入,可以極大的提高高負載下程式的IO效能表現。幾年前,M$在 C# 中便已經通過引入 async/await 關鍵字實現了一套非同步IO機制,成為業界模範, Python現在引入相同的程式設計正規化也算博眾家之長。

事實上,在官方實現原生協程機制前,在目前比較流行的 Python 2.X 版本中,由於 GIL [2] 的存在,Python 程式的多執行緒/多程式效能非常不理想,使得協程成為 Python 併發程式設計的最佳模型,大量的 Python 專案都開始通過使用第三方庫實現的協程編寫程式 (如 eventlet / gevent )、特別是網路程式設計相關的 Python 專案。不過限於 Python 2 語言實現的侷限,協程的實現比較原始,眾多第三方庫的實現並不統一,並且通常都需要 使用一些特殊的程式設計技巧(monkey patch / green 標準庫等手段)才能實現非阻塞IO等特 性來真正提高效能。

相比之下,直接官方提供標準庫原生支援"async io"與協程的 Python 3.5 編寫程式無疑 更加方便。

async/await

官方在 PEP-492 [3] 中定義了 async/await 關鍵字來使用協程。

宣告一個協程非常簡單,通過 async 實現:

async def foo():
    pass複製程式碼

在普通的函式宣告前加上async關鍵字,這個函式就變成了一個協程。

需要注意的是,在 async def 定義的協程內,不能含有 yield 或 yield from 表示式,否則會報 SyntaxError 異常。

await表示等待另一個協程執行完成返回,必須在協程內才能使用。

下面是一個官方文件中提供的協程簡單示例,非常直觀的表示了 async/await、協程 與事件迴圈的執行過程:

import asyncio

async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(1.0)    return x + y

async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result))

loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()複製程式碼

Alt pic

從圖中可以看到,asyncio 標準庫自帶的事件迴圈負責所有協程的排程。在首先執行 print_sum 協程時,內部使用await等待另外一個協程compute的返回結果,於是本地 協程被掛起去執行協程compute。而協程compute執行過程中呼叫了 await asyncio.sleep(1.0),於是本地協程掛起1秒、將程式控制權交回事件迴圈,1秒 後再恢復協程compute的執行直到整個stack執行結束。

而在大量協程併發執行的過程中,除了在協程中主動使用 await,當本地協程發生 IO等待、呼叫 asyncio.sleep()等方法時,程式的控制權也會在不同的協程間切換,從 而在 GIL 的限制下實現最大程度的併發執行,不會由於等待IO等原因導致程式阻塞,達到 較高的效能表現。

值得一提的是,asyncio 和類似 libev 一樣的眾多第三方類庫實現的事件迴圈一樣, 在不同平臺上會使用不同的輪詢機制,比如在Linux平臺上使用 poll/epoll、BSD平臺上 使用 kqueue、NT核心上會直接使用Proactor模式的完成埠。

yield from

如果有一直關注 Python 3 原生協程實現的同學,應該會知道其實它是靠生成器 (Generator)實現的。yield from 則是在 PEP-380 [4] 中新增加的生成器定義 關鍵字,與原生協程的實現密不可分。

原理上 yield from 基本等價於 await,只是 await 針對協程的實現做了更多具體 的處理與約定。

yield from 表示式的語義定義有很長的一串,要徹底搞明白它的實現,需要先學習在 PEP-342 [5] 中描述的增強型生成器(Enhanced Generators),但這裡我們可以簡單的把 它看作一個生成器語法糖:

for v in g:    yield v複製程式碼

加上在生成器內

return value複製程式碼

等價於

raise StopIteration(value)複製程式碼

這樣實現以後,像這樣的表示式

y = f(x)複製程式碼

我們就可以用 yield from 語法將 f(x) 改造成協程實現了

y = yield from g(x)複製程式碼

g 是 f 的生成器,只需要保證兩者最終返回值一致,我們並不關心生成器 g 的中間 狀態。

如果要詳細瞭解這裡的實現,建議還是閱讀 PEP-380 與 PEP-342 原文,裡面有專門的 章節專門描述這個問題。

context manager

PEP-492 中讓人眼前一亮的一點是定義了協程的上下文管理器(context manager),新增 了 async with 語法,讓我們可以將一個上下文作為協程處理,在進入(enter)和退出 (exit)一個 BLOCK 時做協程排程操作:

async with EXPR as VAR:
    BLOCK複製程式碼

與普通的 context manager 相比,async 版本的上下文管理器在內部方法前加了字母 "a"

class AsyncContextManager:
    async def __aenter__(self):
        await log('entering context')

    async def __aexit__(self, exc_type, exc, tb):
        await log('exiting context')複製程式碼

一種典型的應用就是進入臨界區

class Lock:
    async def __aenter__(self):
        await self.lock.lock()

    async def __aexit__(self, exc_type, exc, tb):
        await self.lock.release()


async with Lock():
    ...複製程式碼

類似的語法還有 async for

async for TARGET in ITER:
    BLOCK複製程式碼

這裡不再贅述。

promise/future

promise 是一種最近在 nodejs 流行起來另外一種非同步程式設計正規化,在不同的地方可能也 被稱作 future / deferred,但一般都指的是同一種類似的東西。promise 並沒有 一個非常官方的標準,我瞭解的比較知名的promise標準規範有 Promises A+ [6]。 Python 從 Python 3.4 開始也提供了 asyncio.Future 實現類似的功能。

promise 的核心思想是為一個非同步操作定義操作成功和失敗的不通情況下的的回撥 函式。在 nodejs 這類缺少官方 await 語法支援的語言中,能有效減輕 callback hell 問題,讓程式碼更簡潔,減少 raw callback 寫法導致的縮排太多的 問題,並能方便的實現鏈式操作。

而在 Python 中,語言語法本身提供的功能已經足夠豐富,沒有 callback hell 這類 問題,Future 則可以更專注的讓我們可以將各種非同步操作以一種順序的、更接近人類 邏輯思維與自然語言方式描述出來:

import asyncio@asyncio.coroutinedef slow_operation(future):
    yield from asyncio.sleep(1)
    future.set_result('Future is done!')

loop = asyncio.get_event_loop()
future = asyncio.Future()
asyncio.ensure_future(slow_operation(future))
loop.run_until_complete(future)
print(future.result())
loop.close()複製程式碼

小結

在現在流行的程式語言中,非同步、協程、事件迴圈被越來越多的關注與使用,Python 中的 asyncio.coroutine、Ruby 中的 Fiber、Node 中的 Promise、甚至像 Scala 這樣的語言 中也有了 Future,更不用說 Golang 這種在這條路上一頭走到底的程式語言。 await/Future 這樣的非同步程式設計方式被越來越多的普及與使用, 以後可能會像 if、for 一樣成為程式語言不可缺少的一部分,而協程這種輕量級執行緒在 某些情境下可能也會越來越多的代替目前的多執行緒/多程式成為主流的併發程式設計方式。

美中不足的是,秉承 Guido 一向以來只挖坑不填坑的習慣,Python 3.5 實現新的 async/await 關鍵字後,並沒有給出具體的現有第三方類庫如何向新的 native 協程 實現遷移的方案,更不用說一些流行的 Python 2 第三方類庫目前連 Python 3 都不 支援。距離我們真正能方便流暢地使用體驗它可能還需要一段時間。

References


免費體驗雲安全(易盾)內容安全、驗證碼等服務


更多網易技術、產品、運營經驗分享請點選


相關文章:
【推薦】 Android 模擬器下載、編譯及除錯
【推薦】 spring分散式事務學習筆記(1)


相關文章