本文參考了:

  • How the heck does async/await work in Python 3.5?
  • PEP 380: Syntax for Delegating to a Subgenerator

yield 和 yield from

先讓我們來學習或者回顧一下 yieldyieldfrom的用法。如果你很自信自己完成理解了,可以跳到下一部分。

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

yield from iterator

本質上也就相當於:

for x in iterator:
yield x

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

>>> 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]

理解 yieldfrom對於接下來的部分至關重要。想要完全理解 yieldfrom,還是來看看官方給的例子:

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:

acc=gather_tallies(tallies)這一行開始,由於 gather_tallies函數中有一個 yield,所以不會 while1立即執行(你從視頻中可以看到,acc 是一個 generator 類型)。

next(acc)

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

next(acc)進入到函數體gathertallies,gathertallies中有一個 yieldfromaccumulate(),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 來決定要不要退出循環。在 foriinrange(4)這個for循環裡面,i 都不為 None,所以 while 循環沒有斷。但是,根據我們前面講的:next()會運行到下一個 yield的地方停下來,這個 while 循環一圈,又再次遇到了 yield,所以他會暫停這個函數,把控制權交還給主線程。

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

接下來看第一個 acc.send(None):這時 next變數的值變成了 NoneifnextisNone條件成立,然後返回 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 提案提出了 yieldfrom這個可以理解為語法糖的東西,使得編寫生成器更加簡潔:

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 已經通過 Concurrentprogramming的形式具備了非同步編程的實力了。

Concurrentprogramming只在一個 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 的事件循環使用。

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

break down 一下上面這個代碼:

事件循環開啟了兩個 countdown()協程調用,一直運行到 yieldfromasyncio.sleep(1),這會返回一個 future object,然後暫停,接下來事件循環會一直監視這兩個future object。1秒過後,事件循環就會把 future object send()給coroutine,coroutine又會接著運行,列印出 T-minus2(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/curio,底層的事件循環機制和 asyncio 不一樣, asyncio使用的是 futureobjectcurio使用的是 tuple。同時,這兩個 library 有不同的專註點, asyncio 是一整套的框架, curio則相對更加輕量級,用戶自己需要考慮到事情更多。

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

總結一下

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

最重要的一點感受是:Nothing is Magic。現在你應該能夠對 Python 的協程有了在整體上有了一個把握。

如果你像我一樣真正熱愛計算機科學,喜歡研究底層邏輯,歡迎關注我的微信公眾號:


推薦閱讀:
相关文章