[譯] Python 3.5 協(xié)程究竟是個啥

作為 Python 核心開發(fā)者之一,讓我很想了解這門語言是如何運作的。我發(fā)現(xiàn)總有一些陰暗的角落我對其中錯綜復(fù)雜的細(xì)節(jié)不是很清楚,但是為了能夠有助于 Python 的一些問題和其整體設(shè)計,我覺得我應(yīng)該試著去理解 Python 的核心語法和內(nèi)部運作機制。

但是直到最近我才理解 Python 3.5 中 async/await 的原理。我知道 Python 3.3 中的 yield fromPython 3.4 中的 asyncio 組合得來這一新語法。但較少處理網(wǎng)絡(luò)相關(guān)的問題 - asyncio 并不僅限于此但確是重要用途 - 使我沒太注意 async/await 。我知道:

yield from iterator

(本質(zhì)上)相當(dāng)于:

for x in iterator:
    yield x

我知道 asyncio 是事件循環(huán)框架可以進行異步編程,但是我只是知道這里面每個單詞的意思而已,從沒深入研究 async/await 語法組合背后的原理,我發(fā)現(xiàn)不理解 Python 中的異步編程已經(jīng)對我造成了困擾。因此我決定花時間弄清楚這背后的原理究竟是什么。我從很多人那里得知他們也不了解異步編程的原理,因此我決定寫這篇論文(是的,由于這篇文章花費時間之久以及篇幅之長,我的妻子已經(jīng)將其定義為一篇論文)。

由于我想要正確地理解這些語法的原理,這篇文章涉及到一些關(guān)于 CPython 較為底層的技術(shù)細(xì)節(jié)。如果這些細(xì)節(jié)超出了你想了解的內(nèi)容,或者你不能完全理解它們,都沒關(guān)系,因為我為了避免這篇文章演變成一本書那么長,省略了一些 CPython 內(nèi)部的細(xì)枝末節(jié)(比如說,如果你不知道 code object 有 flags,甚至不知道什么是 code object,這都沒關(guān)系,也不用一定要從這篇文字中獲得什么)。我試著在最后一小節(jié)中用更直接的方法做了總結(jié),如果覺得文章對你來說細(xì)節(jié)太多,你完全可以跳過。

關(guān)于 Python 協(xié)程的歷史課

根據(jù)維基百科給出的定義,“協(xié)程 是為非搶占式多任務(wù)產(chǎn)生子程序的計算機程序組件,協(xié)程允許不同入口點在不同位置暫?;蜷_始執(zhí)行程序”。從技術(shù)的角度來說,“協(xié)程就是你可以暫停執(zhí)行的函數(shù)”。如果你把它理解成“就像生成器一樣”,那么你就想對了。

退回到 Python 2.2,生成器第一次在PEP 255中提出(那時也把它成為迭代器,因為它實現(xiàn)了迭代器協(xié)議)。主要是受到Icon編程語言的啟發(fā),生成器允許創(chuàng)建一個在計算下一個值時不會浪費內(nèi)存空間的迭代器。例如你想要自己實現(xiàn)一個 range() 函數(shù),你可以用立即計算的方式創(chuàng)建一個整數(shù)列表:

def eager_range(up_to):
    """Create a list of integers, from 0 to up_to, exclusive."""
    sequence = []
    index = 0
    while index < up_to:
        sequence.append(index)
        index += 1
    return sequence

然而這里存在的問題是,如果你想創(chuàng)建從0到1,000,000這樣一個很大的序列,你不得不創(chuàng)建能容納1,000,000個整數(shù)的列表。但是當(dāng)加入了生成器之后,你可以不用創(chuàng)建完整的序列,你只需要能夠每次保存一個整數(shù)的內(nèi)存即可。

def lazy_range(up_to):
    """Generator to return the sequence of integers from 0 to up_to, exclusive."""
    index = 0
    while index < up_to:
        yield index
        index += 1

讓函數(shù)遇到 yield 表達(dá)式時暫停執(zhí)行 - 雖然在 Python 2.5 以前它只是一條語句 - 并且能夠在后面重新執(zhí)行,這對于減少內(nèi)存使用、生成無限序列非常有用。

你有可能已經(jīng)發(fā)現(xiàn),生成器完全就是關(guān)于迭代器的。有一種更好的方式生成迭代器當(dāng)然很好(尤其是當(dāng)你可以給一個生成器對象添加 __iter__() 方法時),但是人們知道,如果可以利用生成器“暫停”的部分,添加“將東西發(fā)送回生成器”的功能,那么 Python 突然就有了協(xié)程的概念(當(dāng)然這里的協(xié)程僅限于 Python 中的概念;Python 中真實的協(xié)程在后面才會討論)。將東西發(fā)送回暫停了的生成器這一特性通過 PEP 342添加到了 Python 2.5。與其它特性一起,PEP 342 為生成器引入了 send() 方法。這讓我們不僅可以暫停生成器,而且能夠傳遞值到生成器暫停的地方。還是以我們的 range() 為例,你可以讓序列向前或向后跳過幾個值:

def jumping_range(up_to):
    """Generator for the sequence of integers from 0 to up_to, exclusive.

    Sending a value into the generator will shift the sequence by that amount.
    """
    index = 0
    while index < up_to:
        jump = yield index
        if jump is None:
            jump = 1
        index += jump

if __name__ == '__main__':
    iterator = jumping_range(5)
    print(next(iterator))  # 0
    print(iterator.send(2))  # 2
    print(next(iterator))  # 3
    print(iterator.send(-1))  # 2
    for x in iterator:
        print(x)  # 3, 4

直到PEP 380Python 3.3 添加了 yield from之前,生成器都沒有變動。嚴(yán)格來說,這一特性讓你能夠從迭代器(生成器剛好也是迭代器)中返回任何值,從而可以干凈利索的方式重構(gòu)生成器。

def lazy_range(up_to):
    """Generator to return the sequence of integers from 0 to up_to, exclusive."""
    index = 0
    def gratuitous_refactor():
        while index < up_to:
            yield index
            index += 1
    yield from gratuitous_refactor()

yield from 通過讓重構(gòu)變得簡單,也讓你能夠?qū)⑸善鞔?lián)起來,使返回值可以在調(diào)用棧中上下浮動,而不需對編碼進行過多改動。

def bottom():
    # Returning the yield lets the value that goes up the call stack to come right back
    # down.
    return (yield 42)

def middle():
    return (yield from bottom())

def top():
    return (yield from middle())

# Get the generator.
gen = top()
value = next(gen)
print(value)  # Prints '42'.
try:
    value = gen.send(value * 2)
except StopIteration as exc:
    value = exc.value
print(value)  # Prints '84'.

總結(jié)

Python 2.2 中的生成器讓代碼執(zhí)行過程可以暫停。Python 2.5 中可以將值返回給暫停的生成器,這使得 Python 中協(xié)程的概念成為可能。加上 Python 3.3 中的 yield from,使得重構(gòu)生成器與將它們串聯(lián)起來都很簡單。

什么是事件循環(huán)?

如果你想了解 async/await,那么理解什么是事件循環(huán)以及它是如何讓異步編程變?yōu)榭赡芫拖喈?dāng)重要了。如果你曾做過 GUI 編程 - 包括網(wǎng)頁前端工作 - 那么你已經(jīng)和事件循環(huán)打過交道。但是由于異步編程的概念作為 Python 語言結(jié)構(gòu)的一部分還是最近才有的事,你剛好不知道什么是事件循環(huán)也很正常。

回到維基百科,事件循環(huán) “是一種等待程序分配事件或消息的編程架構(gòu)”?;旧蟻碚f事件循環(huán)就是,“當(dāng)A發(fā)生時,執(zhí)行B”。或許最簡單的例子來解釋這一概念就是用每個瀏覽器中都存在的JavaScript事件循環(huán)。當(dāng)你點擊了某個東西(“當(dāng)A發(fā)生時”),這一點擊動作會發(fā)送給JavaScript的事件循環(huán),并檢查是否存在注冊過的 onclick 回調(diào)來處理這一點擊(“執(zhí)行B”)。只要有注冊過的回調(diào)函數(shù)就會伴隨點擊動作的細(xì)節(jié)信息被執(zhí)行。事件循環(huán)被認(rèn)為是一種循環(huán)是因為它不停地收集事件并通過循環(huán)來發(fā)如何應(yīng)對這些事件。

對 Python 來說,用來提供事件循環(huán)的 asyncio 被加入標(biāo)準(zhǔn)庫中。asyncio 重點解決網(wǎng)絡(luò)服務(wù)中的問題,事件循環(huán)在這里將來自套接字(socket)的 I/O 已經(jīng)準(zhǔn)備好讀和/或?qū)懽鳛椤爱?dāng)A發(fā)生時”(通過selectors模塊)。除了 GUI 和 I/O,事件循環(huán)也經(jīng)常用于在別的線程或子進程中執(zhí)行代碼,并將事件循環(huán)作為調(diào)節(jié)機制(例如,合作式多任務(wù))。如果你恰好理解 Python 的 GIL,事件循環(huán)對于需要釋放 GIL 的地方很有用。

總結(jié)

事件循環(huán)提供一種循環(huán)機制,讓你可以“在A發(fā)生時,執(zhí)行B”?;旧蟻碚f事件循環(huán)就是監(jiān)聽當(dāng)有什么發(fā)生時,同時事件循環(huán)也關(guān)心這件事并執(zhí)行相應(yīng)的代碼。Python 3.4 以后通過標(biāo)準(zhǔn)庫 asyncio 獲得了事件循環(huán)的特性。

asyncawait 是如何運作的

Python 3.4 中的方式

在 Python 3.3 中出現(xiàn)的生成器與之后以 asyncio 的形式出現(xiàn)的事件循環(huán)之間,Python 3.4 通過并發(fā)編程的形式已經(jīng)對異步編程有了足夠的支持。異步編程簡單來說就是代碼執(zhí)行的順序在程序運行前是未知的(因此才稱為異步而非同步)。并發(fā)編程是代碼的執(zhí)行不依賴于其他部分,即便是全都在同一個線程內(nèi)執(zhí)行(并發(fā)不是并行)。例如,下面 Python 3.4 的代碼分別以異步和并發(fā)的函數(shù)調(diào)用實現(xiàn)按秒倒計時。

import asyncio

# Borrowed from http://curio.readthedocs.org/en/latest/tutorial.html.
@asyncio.coroutine
def countdown(number, n):
    while n > 0:
        print('T-minus', n, '({})'.format(number))
        yield from asyncio.sleep(1)
        n -= 1

loop = asyncio.get_event_loop()
tasks = [
    asyncio.ensure_future(countdown("A", 2)),
    asyncio.ensure_future(countdown("B", 3))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

Python 3.4 中,asyncio.coroutine 修飾器用來標(biāo)記作為協(xié)程的函數(shù),這里的協(xié)程是和asyncio及其事件循環(huán)一起使用的。這賦予了 Python 第一個對于協(xié)程的明確定義:實現(xiàn)了PEP 342添加到生成器中的這一方法的對象,并通過[collections.abc.Coroutine這一抽象基類]表征的對象。這意味著突然之間所有實現(xiàn)了協(xié)程接口的生成器,即便它們并不是要以協(xié)程方式應(yīng)用,都符合這一定義。為了修正這一點,asyncio 要求所有要用作協(xié)程的生成器必須asyncio.coroutine修飾。

有了對協(xié)程明確的定義(能夠匹配生成器所提供的API),你可以對任何asyncio.Future對象使用 yield from,從而將其傳遞給事件循環(huán),暫停協(xié)程的執(zhí)行來等待某些事情的發(fā)生( future 對象并不重要,只是asyncio細(xì)節(jié)的實現(xiàn))。一旦 future 對象獲取了事件循環(huán),它會一直在那里監(jiān)聽,直到完成它需要做的一切。當(dāng) future 完成自己的任務(wù)之后,事件循環(huán)會察覺到,暫停并等待在那里的協(xié)程會通過send()方法獲取future對象的返回值并開始繼續(xù)執(zhí)行。

以上面的代碼為例。事件循環(huán)啟動每一個 countdown() 協(xié)程,一直執(zhí)行到遇見其中一個協(xié)程的 yield fromasyncio.sleep() 。這樣會返回一個 asyncio.Future對象并將其傳遞給事件循環(huán),同時暫停這一協(xié)程的執(zhí)行。事件循環(huán)會監(jiān)控這一future對象,直到倒計時1秒鐘之后(同時也會檢查其它正在監(jiān)控的對象,比如像其它協(xié)程)。1秒鐘的時間一到,事件循環(huán)會選擇剛剛傳遞了future對象并暫停了的 countdown() 協(xié)程,將future對象的結(jié)果返回給協(xié)程,然后協(xié)程可以繼續(xù)執(zhí)行。這一過程會一直持續(xù)到所有的 countdown() 協(xié)程執(zhí)行完畢,事件循環(huán)也被清空。稍后我會給你展示一個完整的例子,用來說明協(xié)程/事件循環(huán)之類的這些東西究竟是如何運作的,但是首先我想要解釋一下asyncawait。

Python 3.5 從 yield fromawait

在 Python 3.4 中,用于異步編程并被標(biāo)記為協(xié)程的函數(shù)看起來是這樣的:

# This also works in Python 3.5.
@asyncio.coroutine
def py34_coro():
    yield from stuff()

Python 3.5 添加了types.coroutine 修飾器,也可以像 asyncio.coroutine 一樣將生成器標(biāo)記為協(xié)程。你可以用 async def 來定義一個協(xié)程函數(shù),雖然這個函數(shù)不能包含任何形式的 yield 語句;只有 returnawait 可以從協(xié)程中返回值。

async def py35_coro():
    await stuff()

雖然 asynctypes.coroutine 的關(guān)鍵作用在于鞏固了協(xié)程的定義,但是它將協(xié)程從一個簡單的接口變成了一個實際的類型,也使得一個普通生成器和用作協(xié)程的生成器之間的差別變得更加明確(inspect.iscoroutine() 函數(shù) 甚至明確規(guī)定必須使用 async 的方式定義才行)。

你將發(fā)現(xiàn)不僅僅是 async,Python 3.5 還引入 await 表達(dá)式(只能用于async def中)。雖然await的使用和yield from很像,但await可以接受的對象卻是不同的。await 當(dāng)然可以接受協(xié)程,因為協(xié)程的概念是所有這一切的基礎(chǔ)。但是當(dāng)你使用 await 時,其接受的對象必須是awaitable 對象:必須是定義了__await__()方法且這一方法必須返回一個不是協(xié)程的迭代器。協(xié)程本身也被認(rèn)為是 awaitable 對象(這也是collections.abc.Coroutine 繼承 collections.abc.Awaitable的原因)。這一定義遵循 Python 將大部分語法結(jié)構(gòu)在底層轉(zhuǎn)化成方法調(diào)用的傳統(tǒng),就像 a + b 實際上是a.__add__(b) 或者 b.__radd__(a)。

yield fromawait 在底層的差別是什么(也就是types.coroutineasync def的差別)?讓我們看一下上面兩則Python 3.5代碼的例子所產(chǎn)生的字節(jié)碼在本質(zhì)上有何差異。py34_coro()的字節(jié)碼是:

>>> dis.dis(py34_coro)
  2           0 LOAD_GLOBAL              0 (stuff)
              3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
              6 GET_YIELD_FROM_ITER
              7 LOAD_CONST               0 (None)
             10 YIELD_FROM
             11 POP_TOP
             12 LOAD_CONST               0 (None)
             15 RETURN_VALUE

py35_coro()的字節(jié)碼是:

>>> dis.dis(py35_coro)
  1           0 LOAD_GLOBAL              0 (stuff)
              3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
              6 GET_AWAITABLE
              7 LOAD_CONST               0 (None)
             10 YIELD_FROM
             11 POP_TOP
             12 LOAD_CONST               0 (None)
             15 RETURN_VALUE

忽略由于py34_coro()asyncio.coroutine 修飾器所帶來的行號的差別,兩者之間唯一可見的差異是GET_YIELD_FROM_ITER操作碼 對比GET_AWAITABLE操作碼。兩個函數(shù)都被標(biāo)記為協(xié)程,因此在這里沒有差別。GET_YIELD_FROM_ITER 只是檢查參數(shù)是生成器還是協(xié)程,否則將對其參數(shù)調(diào)用iter()方法(只有用在協(xié)程內(nèi)部的時候yield from所對應(yīng)的操作碼才可以接受協(xié)程對象,在這個例子里要感謝types.coroutine修飾符將這個生成器在C語言層面標(biāo)記為CO_ITERABLE_COROUTINE)。

但是 GET_AWAITABLE的做法不同,其字節(jié)碼像GET_YIELD_FROM_ITER一樣接受協(xié)程,但是接受沒有被標(biāo)記為協(xié)程的生成器。就像前面討論過的一樣,除了協(xié)程以外,這一字節(jié)碼還可以接受awaitable對象。這使得yield fromawait表達(dá)式都接受協(xié)程但分別接受一般的生成器和awaitable對象。

你可能會想,為什么基于async的協(xié)程和基于生成器的協(xié)程會在對應(yīng)的暫停表達(dá)式上面有所不同?主要原因是出于最優(yōu)化Python性能的考慮,確保你不會將剛好有同樣API的不同對象混為一談。由于生成器默認(rèn)實現(xiàn)協(xié)程的API,因此很有可能在你希望用協(xié)程的時候錯用了一個生成器。而由于并不是所有的生成器都可以用在基于協(xié)程的控制流中,你需要避免錯誤地使用生成器。但是由于 Python 并不是靜態(tài)編譯的,它最好也只能在用基于生成器定義的協(xié)程時提供運行時檢查。這意味著當(dāng)用types.coroutine時,Python 的編譯器將無法判斷這個生成器是用作協(xié)程還是僅僅是普通的生成器(記住,僅僅因為types.coroutine這一語法的字面意思,并不意味著在此之前沒有人做過types = spam的操作),因此編譯器只能基于當(dāng)前的情況生成有著不同限制的操作碼。

關(guān)于基于生成器的協(xié)程和async定義的協(xié)程之間的差異,我想說明的關(guān)鍵點是只有基于生成器的協(xié)程可以真正的暫停執(zhí)行并強制性返回給事件循環(huán)。你可能不了解這些重要的細(xì)節(jié),因為通常你調(diào)用的像是asyncio.sleep() function 這種事件循環(huán)相關(guān)的函數(shù),由于事件循環(huán)實現(xiàn)他們自己的API,而這些函數(shù)會處理這些小的細(xì)節(jié)。對于我們絕大多數(shù)人來說,我們只會跟事件循環(huán)打交道,而不需要處理這些細(xì)節(jié),因此可以只用async定義的協(xié)程。但是如果你和我一樣好奇為什么不能在async定義的協(xié)程中使用asyncio.sleep(),那么這里的解釋應(yīng)該可以讓你頓悟。

總結(jié)

讓我們用簡單的話來總結(jié)一下。用async def可以定義得到協(xié)程。定義協(xié)程的另一種方式是通過types.coroutine修飾器 -- 從技術(shù)實現(xiàn)的角度來說就是添加了 CO_ITERABLE_COROUTINE標(biāo)記 -- 或者是collections.abc.Coroutine的子類。你只能通過基于生成器的定義來實現(xiàn)協(xié)程的暫停。

awaitable 對象要么是一個協(xié)程要么是一個定義了__await__()方法的對象 -- 也就是collections.abc.Awaitable -- 且__await__()必須返回一個不是協(xié)程的迭代器。await表達(dá)式基本上與yield from相同但只能接受awaitable對象(普通迭代器不行)。async定義的函數(shù)要么包含return語句 -- 包括所有Python函數(shù)缺省的return None -- 和/或者 await表達(dá)式(yield表達(dá)式不行)。async函數(shù)的限制確保你不會將基于生成器的協(xié)程與普通的生成器混合使用,因為對這兩種生成器的期望是非常不同的。

async/await 看做異步編程的 API

我想要重點指出的地方實際上在我看David Beazley's Python Brasil 2015 keynote之前還沒有深入思考過。在他的演講中,David 指出 async/await 實際上是異步編程的 API (他在 Twitter 上向我重申過)。David 的意思是人們不應(yīng)該將async/await等同于asyncio,而應(yīng)該將asyncio看作是一個利用async/await API 進行異步編程的框架。

David 將 async/await 看作是異步編程的API創(chuàng)建了 curio 項目來實現(xiàn)他自己的事件循環(huán)。這幫助我弄清楚 async/await 是 Python 創(chuàng)建異步編程的原料,同時又不會將你束縛在特定的事件循環(huán)中也無需與底層的細(xì)節(jié)打交道(不像其他編程語言將事件循環(huán)直接整合到語言中)。這允許像 curio 一樣的項目不僅可以在較低層面上擁有不同的操作方式(例如 asyncio 利用 future 對象作為與事件循環(huán)交流的 API,而 curio 用的是元組),同時也可以集中解決不同的問題,實現(xiàn)不同的性能特性(例如 asyncio 擁有一整套框架來實現(xiàn)運輸層和協(xié)議層,從而使其變得可擴展,而 curio 只是簡單地讓用戶來考慮這些但同時也讓它運行地更快)。

考慮到 Python 異步編程的(短暫)歷史,可以理解人們會誤認(rèn)為 async/await == asyncio。我是說asyncio幫助我們可以在 Python 3.4 中實現(xiàn)異步編程,同時也是 Python 3.5 中引入async/await的推動因素。但是async/await 的設(shè)計意圖就是為了讓其足夠靈活從而不需要依賴asyncio或者僅僅是為了適應(yīng)這一框架而扭曲關(guān)鍵的設(shè)計決策。換句話說,async/await 延續(xù)了 Python 設(shè)計盡可能靈活的傳統(tǒng)同時又非常易于使用(實現(xiàn))。

一個例子

到這里你的大腦可能已經(jīng)灌滿了新的術(shù)語和概念,導(dǎo)致你想要從整體上把握所有這些東西是如何讓你可以實現(xiàn)異步編程的稍微有些困難。為了幫助你讓這一切更加具體化,這里有一個完整的(偽造的)異步編程的例子,將代碼與事件循環(huán)及其相關(guān)的函數(shù)一一對應(yīng)起來。這個例子里包含的幾個協(xié)程,代表著火箭發(fā)射的倒計時,并且看起來是同時開始的。這是通過并發(fā)實現(xiàn)的異步編程;3個不同的協(xié)程將分別獨立運行,并且都在同一個線程內(nèi)完成。

import datetime
import heapq
import types
import time

class Task:

    """Represent how long a coroutine should before starting again.

    Comparison operators are implemented for use by heapq. Two-item
    tuples unfortunately don't work because when the datetime.datetime
    instances are equal, comparison falls to the coroutine and they don't
    implement comparison methods, triggering an exception.

    Think of this as being like asyncio.Task/curio.Task.
    """

    def __init__(self, wait_until, coro):
        self.coro = coro
        self.waiting_until = wait_until

    def __eq__(self, other):
        return self.waiting_until == other.waiting_until

    def __lt__(self, other):
        return self.waiting_until < other.waiting_until

class SleepingLoop:

    """An event loop focused on delaying execution of coroutines.

    Think of this as being like asyncio.BaseEventLoop/curio.Kernel.
    """

    def __init__(self, *coros):
        self._new = coros
        self._waiting = []

    def run_until_complete(self):
        # Start all the coroutines.
        for coro in self._new:
            wait_for = coro.send(None)
            heapq.heappush(self._waiting, Task(wait_for, coro))
        # Keep running until there is no more work to do.
        while self._waiting:
            now = datetime.datetime.now()
            # Get the coroutine with the soonest resumption time.
            task = heapq.heappop(self._waiting)
            if now < task.waiting_until:
                # We're ahead of schedule; wait until it's time to resume.
                delta = task.waiting_until - now
                time.sleep(delta.total_seconds())
                now = datetime.datetime.now()
            try:
                # It's time to resume the coroutine.
                wait_until = task.coro.send(now)
                heapq.heappush(self._waiting, Task(wait_until, task.coro))
            except StopIteration:
                # The coroutine is done.
                pass

@types.coroutine
def sleep(seconds):
    """Pause a coroutine for the specified number of seconds.

    Think of this as being like asyncio.sleep()/curio.sleep().
    """
    now = datetime.datetime.now()
    wait_until = now + datetime.timedelta(seconds=seconds)
    # Make all coroutines on the call stack pause; the need to use `yield`
    # necessitates this be generator-based and not an async-based coroutine.
    actual = yield wait_until
    # Resume the execution stack, sending back how long we actually waited.
    return actual - now

async def countdown(label, length, *, delay=0):
    """Countdown a launch for `length` seconds, waiting `delay` seconds.

    This is what a user would typically write.
    """
    print(label, 'waiting', delay, 'seconds before starting countdown')
    delta = await sleep(delay)
    print(label, 'starting after waiting', delta)
    while length:
        print(label, 'T-minus', length)
        waited = await sleep(1)
        length -= 1
    print(label, 'lift-off!')

def main():
    """Start the event loop, counting down 3 separate launches.

    This is what a user would typically write.
    """
    loop = SleepingLoop(countdown('A', 5), countdown('B', 3, delay=2),
                        countdown('C', 4, delay=1))
    start = datetime.datetime.now()
    loop.run_until_complete()
    print('Total elapsed time is', datetime.datetime.now() - start)

if __name__ == '__main__':
    main()

就像我說的,這是偽造出來的,但是如果你用 Python 3.5 去運行,你會發(fā)現(xiàn)這三個協(xié)程在同一個線程內(nèi)獨立運行,并且總的運行時間大約是5秒鐘。你可以將Task,SleepingLoopsleep()看作是事件循環(huán)的提供者,就像asynciocurio所提供給你的一樣。對于一般的用戶來說,只有countdown()main()函數(shù)中的代碼才是重要的。正如你所見,asyncawait或者是這整個異步編程的過程并沒什么黑科技;只不過是 Python 提供給你幫助你更簡單地實現(xiàn)這類事情的API。

我對未來的希望和夢想

現(xiàn)在我理解了 Python 中的異步編程是如何運作的了,我想要一直用它!這是如此絕妙的概念,比你之前用過的線程好太多了。但是問題在于 Python 3.5 還太新了,async/await也太新了。這意味著還沒有太多庫支持這樣的異步編程。例如,為了實現(xiàn) HTTP 請求你要么不得不自己徒手構(gòu)建 ,要么用像是 aiohttp 之類的框架 將 HTTP 添加在另外一個事件循環(huán)的頂端,或者寄希望于更多像hyper一樣的項目不停涌現(xiàn),可以提供對于 HTTP 之類的抽象,可以讓你隨便用任何 I/O 庫 來實現(xiàn)你的需求(雖然可惜的是 hyper目前只支持 HTTP/2)。

對于我個人來說,我希望更多像hyper一樣的項目可以脫穎而出,這樣我們就可以在從 I/O中讀取與解讀二進制數(shù)據(jù)之間做出明確區(qū)分。這樣的抽象非常重要,因為Python多數(shù) I/O 庫中處理 I/O 和處理數(shù)據(jù)是緊緊耦合在一起的。Python 的標(biāo)準(zhǔn)庫 http就有這樣的問題,它不提供 HTTP解析而只有一個連接對象為你處理所有的 I/O。而如果你寄希望于requests可以支持異步編程,那你的希望已經(jīng)破滅了,因為 requests 的同步 I/O 已經(jīng)烙進它的設(shè)計中了。Python 在網(wǎng)絡(luò)堆棧上很多層都缺少抽象定義,異步編程能力的改進使得 Python 社區(qū)有機會對此作出修復(fù)。我們可以很方便地讓異步代碼像同步一樣執(zhí)行,這樣一些填補異步編程空白的工具可以安全地運行在兩種環(huán)境中。

我希望 Python 可以讓 async 協(xié)程支持 yield。或者需要用一個新的關(guān)鍵詞來實現(xiàn)(可能像 anticipate之類?),因為不能僅靠async就實現(xiàn)事件循環(huán)讓我很困擾。幸運的是,我不是唯一一個這么想的人,而且PEP 492的作者也和我意見一致,我覺得還是有機會可以移除掉這點小瑕疵。

結(jié)論

基本上 asyncawait 產(chǎn)生神奇的生成器,我們稱之為協(xié)程,同時需要一些額外的支持例如 awaitable 對象以及將普通生成器轉(zhuǎn)化為協(xié)程。所有這些加到一起來支持并發(fā),這樣才使得 Python 更好地支持異步編程。相比類似功能的線程,這是一個更妙也更簡單的方法。我寫了一個完整的異步編程例子,算上注釋只用了不到100行 Python 代碼 -- 但仍然非常靈活與快速(curio FAQ 指出它比 twisted 要快 30-40%,但是要比 gevent 慢 10-15%,而且全部都是有純粹的 Python 實現(xiàn)的;記住Python 2 + Twisted 內(nèi)存消耗更少同時比Go更容易調(diào)試,想象一下這些能幫你實現(xiàn)什么吧?。?。我非常高興這些能夠在 Python 3 中成為現(xiàn)實,我也非常期待 Python 社區(qū)可以接納并將其推廣到各種庫和框架中區(qū),可以使我們都能夠受益于 Python 異步編程帶來的好處!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容