小議Python3的原生協程機制

此文已由做者張耕源受權網易雲社區發佈。
html

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


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

IO多路複用與協程的引入,能夠極大的提升高負載下程序的IO性能表現。幾年前,M$在 C# 中便已經經過引入 async/await 關鍵字實現了一套異步IO機制,成爲業界模範, Python如今引入相同的編程範式也算博衆家之長。git

事實上,在官方實現原生協程機制前,在目前比較流行的 Python 2.X 版本中,因爲 GIL [2] 的存在,Python 程序的多線程/多進程性能很是不理想,使得協程成爲 Python 併發編程的最佳模型,大量的 Python 項目都開始經過使用第三方庫實現的協程編寫程序 (如 eventlet / gevent )、特別是網絡編程相關的 Python 項目。不過限於 Python 2 語言實現的侷限,協程的實現比較原始,衆多第三方庫的實現並不統一,而且一般都須要 使用一些特殊的編程技巧(monkey patch / green 標準庫等手段)才能實現非阻塞IO等特 性來真正提升性能。github

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

async/await

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

聲明一個協程很是簡單,經過 async 實現:promise

async def foo():
    pass複製代碼

在普通的函數聲明前加上async關鍵字,這個函數就變成了一個協程。安全

須要注意的是,在 async def 定義的協程內,不能含有 yield 或 yield from 表達式,不然會報 SyntaxError 異常。bash

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)

相關文章
相關標籤/搜索