回應 https://gist.github.com/rayshih/4144d6b8bc045fc26daf8887bd0cb4e2。
我一直覺得你的發言和其他人沒有交集,但想不通到底哪裡出了問題。但看了這段程式之後,我覺得有點懂了。原文的程式不容易看 timestamp(加上 1th 2th 3th 讓我豆頁痛),所以我稍微改寫如下。
import asyncio
import time
begin = time.time()
def async_generator():
@asyncio.coroutine
def count_down(i, c):
print('Run {}, countdown {} (clock {:.5f})'.format(i, c, time.time() - begin))
if c == 0:
return
yield from asyncio.sleep(0.5)
yield from count_down(i, c - 1)
i = 1
while i <= 5:
yield count_down(i, i)
i += 1
@asyncio.coroutine
def run_all(asyncGen):
for a in asyncGen:
yield from a
loop = asyncio.get_event_loop()
loop.run_until_complete(run_all(async_generator()))
loop.close()
輸出:
Run 1, countdown 1 (clock 0.00040)
Run 1, countdown 0 (clock 0.50487)
Run 2, countdown 2 (clock 0.50493)
Run 2, countdown 1 (clock 1.00741)
Run 2, countdown 0 (clock 1.50859)
Run 3, countdown 3 (clock 1.50867)
Run 3, countdown 2 (clock 2.00978)
Run 3, countdown 1 (clock 2.51487)
Run 3, countdown 0 (clock 3.01876)
Run 4, countdown 4 (clock 3.01882)
Run 4, countdown 3 (clock 3.52049)
Run 4, countdown 2 (clock 4.02429)
Run 4, countdown 1 (clock 4.52681)
Run 4, countdown 0 (clock 5.02723)
Run 5, countdown 5 (clock 5.02733)
Run 5, countdown 4 (clock 5.52934)
Run 5, countdown 3 (clock 6.03095)
Run 5, countdown 2 (clock 6.53256)
Run 5, countdown 1 (clock 7.03788)
Run 5, countdown 0 (clock 7.54188)
或許上面這樣會比較容易看出來:在這段程式裡,所有的 coroutines 是被依序執行,沒有交錯。這整段程式根本是 synchrounous,只是用了 asyncio
來排程。它其實完全等同於下面的程式:
import time
begin = time.time()
def count_down(i):
for c in range(i, -1, -1):
print('Run {}, countdown {} (clock {:.5f})'.format(i, c, time.time() - begin))
if c == 0:
break
time.sleep(0.5)
yield
for i in range(1, 6):
for _ in count_down(i):
pass
我之前提過一個觀點:
Async program 不符合人類習慣思維,才會被說一開始的學習 overhead 很高,寫起來容易卡。尤其如果同步異步 paradigms 並立(例如 Python),就更容易在切換的時候出問題。
一般人習慣的就是 synchronous 寫法,所以即使用了 async API,仍然需要一直注意,才不會不小心寫出實質上根本不 async 的程式——而且完全不會注意到。這些問題需要親身經歷,而且即使經驗豐富的人,也免不了犯這個錯。甚至可以說,只有不知道自己有這個盲點的人,才會天真地相信 async 程式容易寫。
如果用 async-await keywords 改寫前面的程式,就會像下面這樣:
import asyncio
import time
begin = time.time()
def async_generator():
async def count_down(i, c):
print('Run {}, countdown {} (clock {:.5f})'.format(i, c, time.time() - begin))
if c == 0:
return
await asyncio.sleep(0.5)
await count_down(i, c - 1)
i = 1
while i <= 5:
yield count_down(i, i)
i += 1
async def run_all(asyncGen):
for a in asyncGen:
await a
loop = asyncio.get_event_loop()
loop.run_until_complete(run_all(async_generator()))
loop.close()
首先,coroutine
decorator 可以被 async def
關鍵字取代。接著 asyncio.sleep
與 count_down
是 coroutines,所以對它們的 yield from
可以用 await
取代。
從上面的程式可以看出來,async_generator
是一個 generator,然後它 generate 的東西(yield count_down(i, i)
)是 coroutine。它是 a generator that generates coroutines,但本身並不 async。Async generator 的定義與它 generate 出來的東西無關,而是需要在 generate 東西的時候是 async。這個意思是,如果你有複數個 async generators,它們應該要可以交錯著 generate 東西出來,而不是循序進行(一個跑完之後,才開始跑下一個)。上面的程式只有用 generator 一次,所以看不出來,不過如果多用幾次:
# 無關程式省略。
async def run_all(*gens):
for g in gens:
for a in g:
await a
loop.run_until_complete(run_all(async_generator(), async_generator()))
就很明顯了。這個 generator function 產生的 generators 只能循序被 exhaust,所以不是 async generators。
Mosky 叫我不要開嘲諷,我自己也覺得會讓人覺得嘲諷,可是該講的還是得講。
你根本不懂 asynchrony。而且你根本不知道自己不懂。
Syntax 不是 async programming 的重點,程式究竟怎麼動才是。我想不出更好的說法,但這不是在嗆你,是事實。就因為這樣,使得你沒抓到我要討論的點,所以我才會覺得這麼卡。我們討論的不是同一件事。你需要從頭想過 async 的目的,以及 coroutine 的原理。有了這些概念,才能知道一個程式為什麼要變得 async,以及應該如何變得 async。
抱歉我只對 Python 資源比較熟,而 async 在 Python 仍然是很新的概念,所以資源還是以影片居多。下面有一些我常推薦的概念性影片,或許會對你有幫助。有興趣可以看一看,然後更重要的,請理解。