我就直接回在下面了
回應 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
其實並不相同,最大的差異是在:要同時執行 count_down 這個 function,以上這個版本必須要開 thread。而使用 asyncio 提供的 event loop 的版本不需要即可做到 concurrent 執行。 詳情請往下看。
我之前提過一個觀點:
Async program 不符合人類習慣思維,才會被說一開始的學習 overhead 很高,寫起來容易卡。尤其如果同步異步 paradigms 並立(例如 Python),就更容易在切換的時候出問題。
一般人習慣的就是 synchronous 寫法,所以即使用了 async API,仍然需要一直注意,才不會不小心寫出實質上根本不 async 的程式——而且完全不會注意到。這些問題需要親身經歷,而且即使經驗豐富的人,也免不了犯這個錯。甚至可以說,只有不知道自己有這個盲點的人,才會天真地相信 async 程式容易寫。
容不容易寫是相對的,不過這不是我要討論的重點,原討論題目是:
「想知道你覺得 async/await 不好寫的理由是?」,但不知道為什麼你突然覺得我要 argue「 async 程式容易寫」。
附上我本來的問題,順便把問題來回到 async-await syntax:
Ray Shih: Mosky Liu 我覺得站在 javascript 開發者的角度來看,從 nodejs nonblocking callback -> promise -> async/await,大家似乎都覺得 async/await 是最好用的。想知道你覺得 async/await 不好寫的理由是?
(雖然我覺得 generator function 就夠了)
如果用 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 API 以 generator 實作,並不代表不能實作 async 的 generator。假設 async API 的 type 是
Promise
,而 generator object 的 type 是Generator
,那「async 的 generator」就的 type 就是Generator<Promise>
。
因為你當初提出的挑戰上面寫的是「async 的 generator」,也就是 type 是
Generator<Future>
先做一點小小補充:
- 因為有網友反應 python 沒有 Promise 所以我把它換成了 Future,不過再次強調,因為 python 是 duck typing ,並沒有這種 syntax,所以我是借用 Java 的 generic type 來描述。
- 其實寫精確一點應該是:
Generator<Future<T>>
而這個版本確確實實符合這個描述。不過看起來是我誤會你的意思了,你要的是 async generator,而不是「async 的 generator」。不過沒有關係 async generator 仍舊可以使用 generator 來實作,而 type 是:
-- this is haskell type declaration
data Op a = Await Future a | Emit a | AsyncFor (AsyncGenerator b) (b -> AsyncGenerator a)
type AsyncGenerator a = Generator (Op a)
再次說聲抱歉,如果不借用 Haskell 的語法,會變得又臭又長。不過大部分的人對 Haskell 不熟,所以我還是稍微解釋一下:
之前的「async 的 generator」的 type 是 Generator<Future<T>>
,因為這樣只能做 "await" 的動作,並不能做 "emit value" 給外界(也就是 generate value) ,所以我們需要讓 Generator 不只「吐出」Future
還要讓他能「吐出」"emit value" 的「指令」。也就是 AsyncGenerator a
可以說是延伸版的 Generator (Future a)
。
跟執行 Generator (Future a)
時需要的 run_all
function 一樣,我們一樣需要另外的 runner function,必須要承認這樣的 runner function 並不好寫,不過是一個有趣的練習題,以下是我的實作:
https://gist.github.com/rayshih/0908eb6bd85891d4e26ee6f391f6f937
(因為有點長,所以只貼連結。)
至於測試程式的來源,是參考 Willian Fan 的留言:
https://www.facebook.com/photo.php?fbid=1723296854349009&set=p.1723296854349009&type=3&theater
Willian Fan 所提供的 library https://github.com/njsmith/async_generator,的實作是以 async-await syntax 為基礎,建立 async generator 的 polyfill,並不能作為「我覺得 Generator 就夠用」的憑據,而我上面的實作並沒有使用 async-await syntax,可以說是只用 generator 的 async generator polyfill。
# 無關程式省略。 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。
回到這是不是 async program 的問題:
而是需要在 generate 東西的時候是 async。這個意思是,如果你有複數個 async generators,它們應該要可以交錯著 generate 東西出來
如果你要看到交錯行為的話,不需要改寫 run_all
,因為 asyncio 本來就有提供這樣的 function
loop.run_until_complete(
asyncio.gather(
run_all(async_generator()),
run_all(async_generator())
)
)
這樣做就有「交錯」了,而且只用到一個 event loop,所以他是一個 1 thread, nonblocking, concurrent program,不知道這樣有沒有符合你心目中的 async?
Mosky 叫我不要開嘲諷,我自己也覺得會讓人覺得嘲諷,可是該講的還是得講。
你根本不懂 asynchrony。而且你根本不知道自己不懂。
Syntax 不是 async programming 的重點,程式究竟怎麼動才是。我想不出更好的說法,但這不是在嗆你,是事實。就因為這樣,使得你沒抓到我要討論的點,所以我才會覺得這麼卡。我們討論的不是同一件事。你需要從頭想過 async 的目的,以及 coroutine 的原理。有了這些概念,才能知道一個程式為什麼要變得 async,以及應該如何變得 async。
是的 Syntax 不是 async programming 的重點,重點是背後的 programming model,所以雖然我最初問的問題是「想知道你覺得 async/await 不好寫的理由是?」其實問的不只是 syntax,更多的是想知道為何你們覺得這樣的 async model 是比其他 async model 更難使用。
抱歉我只對 Python 資源比較熟,而 async 在 Python 仍然是很新的概念,所以資源還是以影片居多。下面有一些我常推薦的概念性影片,或許會對你有幫助。有興趣可以看一看,然後更重要的,請理解。
感謝提供這些資源。分享這篇作為回禮
這幾天我也想了不少,的確 generator 不是萬能的,而且藉由上面的練習,讓我重新思考 generator 的本質,也查到了一些 generator 的限制,不過都不影響我之前提到的論述。
上面的 Async Generator by Generator only
,我覺得滿值得寫講解文的,我先列入 Todo,有興趣的,不好意思,麻煩等等。
跟之前一樣,如果我有什麼東西講的不清楚,或是寫錯的地方,麻煩再請指教,謝謝。
- Update: Fix format