異步編程 101:Python async await發展簡史

本文參考了:html

yield 和 yield from

先讓咱們來學習或者回顧一下yieldyield from的用法。若是你很自信本身完成理解了,能夠跳到下一部分。python

Python3.3提出了一種新的語法:yield fromgit

yield from iterator
複製代碼

本質上也就至關於:github

for x in iterator:
    yield x
複製代碼

下面的這個例子中,兩個 yield from加起來,就組合獲得了一個大的iterable(例子來源於官網3.3 release):golang

>>> def g(x):
...     yield from range(x, 0, -1)
...     yield from range(x)
...
>>> list(g(5))
[5, 4, 3, 2, 1, 0, 1, 2, 3, 4]
複製代碼

理解 yield from對於接下來的部分相當重要。想要徹底理解 yield from,仍是來看看官方給的例子:編程

def accumulate():
    tally = 0
    while 1:
        next = yield
        if next is None:
            return tally
        tally += next


def gather_tallies(tallies):
    while 1:
        tally = yield from accumulate()
        tallies.append(tally)

tallies = []
acc = gather_tallies(tallies)
next(acc) # Ensure the accumulator is ready to accept values

for i in range(4):
    acc.send(i)
acc.send(None) # Finish the first tally

for i in range(5):
    acc.send(i)
acc.send(None) # Finish the second tally
print(tallies)
複製代碼

我還專門爲此錄製了一段視頻,你能夠配合文字一塊兒看,或者你也能夠打開 pycharm 以及任何調試工具,本身調試一下。 視頻連接瀏覽器

來一塊兒 break down:bash

acc = gather_tallies(tallies)這一行開始,因爲gather_tallies函數中有一個 yield,因此不會while 1當即執行(你從視頻中能夠看到,acc 是一個 generator 類型)。微信

next(acc)網絡

next()會運行到下一個 yield,或者報StopIteration錯誤。

next(acc)進入到函數體gather_tallies,gather_tallies中有一個yield from accumulate(),next(acc)不會在這一處停,而是進入到『subgenerator』accumulate裏面,而後在next = yield處,遇到了yield,而後暫停函數,返回。

for i in range(4):
    acc.send(i)
複製代碼

理解一下 acc.send(value)有什麼用:

  • 第一步:回到上一次暫停的地方
  • 第二步:把value 的值賦給 xxx = yield 中的xxx,這個例子中就是next

accumulate函數中的那個while 循環,經過判斷next的值是否是 None 來決定要不要退出循環。在for i in range(4)這個for循環裏面,i 都不爲 None,因此 while 循環沒有斷。可是,根據咱們前面講的:next()會運行到下一個 yield的地方停下來,這個 while 循環一圈,又再次遇到了yield,因此他會暫停這個函數,把控制權交還給主線程。

理清一下:對於accumulate來講,他的死循環是沒有結束的,下一次經過 next()恢復他運行時,他仍是在運行他的死循環。對於gather_tallies來講,他的yield from accumulate()也還沒運行完。對於整個程序來講,確實在主進程和accumulate函數體之間進行了屢次跳轉。

接下來看第一個acc.send(None):這時next變量的值變成了Noneif next is None條件成立,而後返回tally給上一層函數。(計算一下,tally 的值爲0 + 1 + 2 + 3 = 6)。這個返回值就賦值給了gather_tallies中的gally。這裏須要注意的是,gather_tallies的死循環還沒結束,因此此時調用next(acc)不會報StopIteration錯誤。

for i in range(5):
    acc.send(i)
acc.send(None) # Finish the second tally
複製代碼

這一部分和前面的邏輯是同樣的。acc.send(i)會先進入gather_tallies,而後進入accumulate,把值賦給nextacc.send(None)中止循環。最後tally的值爲10(0 + 1 + 2 + 3 + 4)。

最終tallies列表爲:[6,10]

Python async await發展簡史

看一下 wikipedia 上 Coroutine的定義:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed.

關鍵點在於by allowing execution to be suspended and resumed.(讓執行能夠被暫停和被恢復)。通俗點說,就是:

coroutines are functions whose execution you can pause。(來自How the heck does async/await work in Python 3.5?

這不就是生成器嗎?

python2.2 - 生成器起源

Python生成器的概念最先起源於 python2.2(2001年)時剔除的 pep255,受Icon 編程語言啓發。

生成器有一個好處,不浪費空間,看下面這個例子:

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
複製代碼

若是用這個函數生成一個10W 長度的列表,須要等待 while 循環運行結束返回。而後這個sequence列表將會佔據10W 個元素的空間。耗時不說(從可以第一次可以使用到 sequence 列表的時間這個角度來看),佔用空間還很大。

藉助上一部分講的yield,稍做修改:

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
複製代碼

這樣就只須要佔據一個元素的空間了,並且當即就能夠用到 range,不須要等他所有生成完。

python2.5 : send stuff back

一些有先見之明的前輩想到,若是咱們可以利用生成器可以暫停的這一特性,而後想辦法添加 send stuff back 的功能,這不就符合維基百科對於協程的定義了麼?

因而就有了pep342

pep342中提到了一個send()方法,容許咱們把一個"stuff"送回生成器裏面,讓他接着運行。來看下面這個例子:

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
複製代碼

這裏的send把一個『stuff』送進去給生成器,賦值給 jump,而後判斷jump 是否是 None,來執行對應的邏輯。

python3.3 yield from

自從Python2.5以後,關於生成器就沒作什麼大的改進了,直到 Python3.3時提出的pep380。這個 pep 提案提出了yield from這個能夠理解爲語法糖的東西,使得編寫生成器更加簡潔:

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

第一節咱們已經詳細講過 yield from 了,這裏就不贅述了。

python3.4 asyncio模塊

插播:事件循環(eventloop)

若是你有 js 編程經驗,確定對事件循環有所瞭解。

理解一個概念,最好也是最有bigger的就是翻出 wikipedia:

an event loop "is a programming construct that waits for and dispatches events or messages in a program" - 來源於Event loop - wikipedia

簡單來講,eventloop 實現當 A 事件發生時,作 B 操做。拿瀏覽器中的JavaScript事件循環來講,你點擊了某個東西(A 事件發生了),就會觸發定義好了的onclick函數(作 B 操做)。

在 Python 中,asyncio 提供了一個 eventloop(回顧一下上一篇的例子),asyncio 主要聚焦的是網絡請求領域,這裏的『A 事件發生』主要就是 socket 能夠寫、 socket能夠讀(經過selectors模塊)。

到這個時期,Python 已經經過Concurrent programming的形式具有了異步編程的實力了。

Concurrent programming只在一個 thread 裏面執行。go 語言blog 中有一個很是不錯的視頻:Concurrency is not parallelism,很值得一看。

這個時代的 asyncio 代碼

這個時期的asyncio代碼是這樣的:

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()
複製代碼

輸出結果爲:

T-minus 2 (A)
T-minus 3 (B)
T-minus 1 (A)
T-minus 2 (B)
T-minus 1 (B)
複製代碼

這時使用的是asyncio.coroutine修飾器,用來標記某個函數能夠被 asyncio 的事件循環使用。

看到yield from asyncio.sleep(1)了嗎?經過對一個asyncio.Future object yield from,就把這個future object 交給了事件循環,當這個 object 在等待某件事情發生時(這個例子中就是等待 asyncio.sleep(1),等待 1s 事後),把函數暫停,開始作其餘的事情。當這個future object 等待的事情發生時,事件循環就會注意到,而後經過調用send()方法,讓它從上次暫停的地方恢復運行。

break down 一下上面這個代碼:

事件循環開啓了兩個countdown()協程調用,一直運行到yield from asyncio.sleep(1),這會返回一個 future object,而後暫停,接下來事件循環會一直監視這兩個future object。1秒事後,事件循環就會把 future object send()給coroutine,coroutine又會接着運行,打印出T-minus 2 (A)等。

python3.5 async await

python3.4的

@asyncio.coroutine
def py34_coro():
    yield from stuff()
複製代碼

到了 Python3.5,能夠用一種更加簡潔的語法表示:

async def py35_coro():
    await stuff()
複製代碼

這種變化,從語法上面來說並沒什麼特別大的區別。真正重要的是,是協程在 Python 中哲學地位的提升。 在 python3.4及以前,異步函數更多就是一種很普通的標記(修飾器),在此以後,協程變成了一種基本的抽象基礎類型(abstract base class):class collections.abc.Coroutine

How the heck does async/await work in Python 3.5?一文中還講到了asyncawait底層 bytecode 的實現,這裏就不深刻了,畢竟篇幅有限。

把 async、await看做是API 而不是 implementation

Python 核心開發者(也是我最喜歡的 pycon talker 之一)David M. Beazley在PyCon Brasil 2015的這一個演講中提到:咱們應該把 asyncawait看做是API,而不是實現。 也就是說,asyncawait不等於asyncioasyncio只不過是asyncawait的一種實現。(固然是asyncio使得異步編程在 Python3.4中成爲可能,從而推進了asyncawait的出現)

他還開源了一個項目github.com/dabeaz/curi…,底層的事件循環機制和 asyncio 不同,asyncio使用的是future objectcurio使用的是tuple。同時,這兩個 library 有不一樣的專一點,asyncio 是一整套的框架,curio則相對更加輕量級,用戶本身須要考慮到事情更多。

How the heck does async/await work in Python 3.5?此文還有一個簡單的事件循環實現例子,有興趣能夠看一下,後面有時間的話也許會一塊兒實現一下。

總結一下

  • 協程只有一個 thread。
  • 操做系統調度進程、協程用事件循環調度函數。
  • async、await 把協程在 Python 中的哲學地位提升了一個檔次。

最重要的一點感覺是:Nothing is Magic。如今你應該可以對 Python 的協程有了在總體上有了一個把握。

若是你像我同樣真正熱愛計算機科學,喜歡研究底層邏輯,歡迎關注個人微信公衆號:

相關文章
相關標籤/搜索