Skip to content

Instantly share code, notes, and snippets.

@rayshih
Forked from uranusjr/沒有交集啊.md
Last active April 13, 2018 00:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rayshih/f716af730e96c321098dc822c15af16f to your computer and use it in GitHub Desktop.
Save rayshih/f716af730e96c321098dc822c15af16f to your computer and use it in GitHub Desktop.

我就直接回在下面了

回應 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 就夠了)

而且它根本沒有實作 Asynchronous Generator

如果用 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.sleepcount_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>

先做一點小小補充:

  1. 因為有網友反應 python 沒有 Promise 所以我把它換成了 Future,不過再次強調,因為 python 是 duck typing ,並沒有這種 syntax,所以我是借用 Java 的 generic type 來描述。
  2. 其實寫精確一點應該是: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 仍然是很新的概念,所以資源還是以影片居多。下面有一些我常推薦的概念性影片,或許會對你有幫助。有興趣可以看一看,然後更重要的,請理解

感謝提供這些資源。分享這篇作為回禮

Conclusion

這幾天我也想了不少,的確 generator 不是萬能的,而且藉由上面的練習,讓我重新思考 generator 的本質,也查到了一些 generator 的限制,不過都不影響我之前提到的論述。

上面的 Async Generator by Generator only,我覺得滿值得寫講解文的,我先列入 Todo,有興趣的,不好意思,麻煩等等。

跟之前一樣,如果我有什麼東西講的不清楚,或是寫錯的地方,麻煩再請指教,謝謝。

  • Update: Fix format
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment