Skip to content

Instantly share code, notes, and snippets.

@rayshih
Forked from uranusjr/async-async-async.md
Last active July 8, 2019 04:09
Show Gist options
  • Save rayshih/e7d736e19d172d1e1ebb127ffc26fe2b to your computer and use it in GitHub Desktop.
Save rayshih/e7d736e19d172d1e1ebb127ffc26fe2b to your computer and use it in GitHub Desktop.

Re: asyncio 好寫嗎?

首先,其實還滿開心有機會可以做個小小辯論的啦。我的回應如下

這把好幾個主題混在一起了,需要一個一個看。

1. Async Program 好寫嗎?

不好寫。東西好不好寫的意思,代表它和 programmer 的內在思考模式符合程度。Async program 不符合人類習慣思維,所以才會被說一開始的學習 overhead 很高,寫起來容易卡。尤其如果同步與異步 paradigms 並立(例如 Python),就更容易在切換的時候出問題。

一開始學習的 overhead 很高不代表寫起來就會很卡,舉例來說不少人覺得 Rails 寫起來很順手,但大部分的人都說相對學習門檻比較高。我的確有一段時期不習慣 async program,但現在是完全沒有問題的。當然這只能代表我個人經驗,不過同時也是我要問的問題:你覺得 Async Program 不好寫,所以是哪裡不好寫?你的回答是「不符合人類習慣思維」。

2. Async-await 好用嗎?

看你跟誰比。正常人都比較習慣 synchronous programming,所以 async program 的寫法越 synchronous,感覺就會越好用。Function reference callback → Anonymous function callback → Promise API → Async-await 這整個系統本身就是在把 async program 寫得越來越像 synchronous,所以當然是越後面越好用。

其實我覺得真正的重點在於 composition,而不是看起來是不是 synchronous code。 async await 再怎麼樣都只是 syntax sugar 而已。

但上面只考慮 single-threaded non-blocking API。我自己的觀察是因為發展 promised-based system 的先鋒大多是 single-threaded (e.g. Lua 和 JavaScript),所以不會這樣比較,但以 Python 社群的角度,async-await 面對的比較對象是 multi-threading 與 multi-processing,而這兩個都比 coroutine 啊 promise 啊 event loop 這些東西容易理解得多,也就更容易上手。

實際上 Promise 只是其中比較有名的代表,使用物件來表達 async operation 的執行過程的,也不是只有只支援 single-threaded 的程式語言,舉例來說 RxJava,所以我並不認為 promise(或類似的,像 Future/Task) 只適用於 single thread 環境。因此我認為用 single-threaded、 multi-threading 來作為分水嶺並不恰當。

另外,multi-threading 與 multi-processing 在稍微複雜一點的環境下常常需要解決 lock 的問題,而使用 coroutine/promise/evet loop 等等,都傾向 immutable data,所以比較沒有 lock 的問題,在此為前提之下,我完全不認同 multi-thread/process 有比較好理解,而容易上手常常只是初學者的錯覺。當然如果是要深究到 fine-grain 資源管理的話,那當然還是得回到 multi-thread/process,不過我們在討論的是易用,而不是效能極限。

3. asyncio 好用嗎?

不好用。它的內部架構有些地方很繞(例如我就不懂為什麼 FutureTask 要是兩個不同的類型),介面也不 consistent(有時候只吃 coroutine 有時候只吃 future 大部分時候又都可以),不過更大的問題是對一般使用者而言暴露出太多底層。

不錯,這才是應該有的回答,明確點出 library 抽象滲漏問題

這和 asyncio 的設計目的有關。如果你去聽 Guido 在 PyCon US 的演講asyncio(那時候還叫 Tulip)的主要角色是為現存系統(例如 Twisted 和 Tornado)提供共同 event loop implementation。所以你讀 PEP 3156 時會發現很大一段篇幅在解釋每一層的 API,甚至還有專門一段在講 interoperability。這都顯示 asyncio 的主要 audience 不是 end users,而是把實作優秀 interface 的任務交給第三方套件——例如 Tornado。

Generator 夠了

夠什麼?Generator 有它能做和不能做的事。它是 asyncio(和檯面上所有 Python 的 async-await 函式庫)實作 Promise API 的基礎,所以你也可以用它取代很多 asyncio 的功能。但只用純 Python 的 generator 沒辦法為 Python 的所有功能實作 async variant,因為這會有雞生蛋生雞問題。最簡單的矛盾:如果你只用 generator 實作 async API,那要怎麼實作 async 的 generator?

其實沒有雞生蛋蛋生雞的問題, async API 以 generator 實作,並不代表不能實作 async 的 generator。假設 async API 的 type 是 Promise,而 generator object 的 type 是 Generator,那「async 的 generator」就的 type 就是 Generator<Promise>。當然我還沒研究 Python 是不是有什麼歷史因素導致不好或甚至不能這樣做,但其他語言的實作你可以參考 Haskell 的 Monad Transformer 或是我之前貼的連結

https://curiosity-driven.org/monads-in-javascript#do

JavaScript、Python、別的語言,論述不變

這沒有道理,因為每個語言(和內建函式庫)都有自己的特性,把一樣的東西架上去時,就需要考慮不一樣的 interoperability。Python 人和 C# 人重疊性很低,然後大部分人都不太熟 Lua(我也不熟),所以就用 JavaScript 當例子和 Python 比較吧。在 JavaScript 實作 Promise/A+ 與 Python 實作 asyncio 底層時的差異可以分成兩部分來討論:

同意,當然每個語言都有不同的特性,比較時的確需要考慮,這也是為什麼我要問這些問題。

1. JavaScript 只有一個 event loop 實作,而且你看不到

JavaScript 從語言初期就隱含一個 event loop,所有的 ecosystem 也都圍繞這個性質發展。Python 沒有,所以 Python 的函式庫有的不考慮 event loop,也有的實作自己的 event loop。前面講過,asyncio 的初始目的就是為了統一它們,但因為少了 JavaScript 的假設,實作與介面就必須非常 explicit,用起來就很卡。

如果我上面所說的,JavaScript 的 implicit event loop 並不能作為 Async Programming 特別好寫的理由。一樣拿 RxJava 為例子,在 Java 的環境下,雖然必須要 explicit 的描述 thread 的切換,但並不影響他使得 Async Programming 更容易撰寫的論述。

2. JavaScrtipt 有執行緒限制、Python 沒有

因為 JavaScript 保證 one and only one event loop,async function 和 promise API、callback-style API 的對接非常乾淨。當你 await 你建立的 Promise 時,很明顯是要在那個 event loop 執行;在 asyncio 裡,同樣的事情就會變得非常繞,因為 Python 擁有完整的執行緒實作,且每個執行緒都可以擁有自己的 event loop,卻又沒有保證會有。

同上。

舉個例子

如果我想用 asyncio.sleep 定時,每五秒印出一段字,就得這樣做:

import asyncio

def run_once(previous_future=None):
    print('message')
    future = asyncio.ensure_future(asyncio.sleep(5))
    future.add_done_callback(run_once)

loop = asyncio.get_event_loop()
loop.call_soon(run_once)
loop.run_forever()

當然以延遲這個 case 而言,asyncio 有更方便的 loop.call_later,不過一般來說 coroutine 與 synchronous function 就得這樣接。我個人是覺得這樣已經有點煩了,不過注意一下 ensure_future 那行。前面有說 Python 可以擁有多個 event loops,那要怎麼知道是哪一個?這就是 get_event_loop 的用意;當程式初始化時,asyncio 會自動建立一個預設的 event loop;絕大部分 asyncio 關於 future 與 task 的 API 都吃一個 loop keyword argument,讓你可以指定要把它跑在哪個 event loop 上;如果不指定(像上面的狀況),就會用預設的。

這也是為什麼 asyncio 沒辦法像 ES6 一樣,async function 可以有透明的 promise API,能夠直接對接。Coroutine 沒有關於 event loop 的資訊,不知道自己要怎麼執行;你必須用它建立一個 task(通常用 ensure_future,至於為什麼名字是 future 建出來卻是 task 你問 Guido 吧我也很想知道),然後執行那個 task。

這裡面還有很多很多講不完的雷,不過上面應該夠證明 async 的實作標準不是所有語言通用了。

我看起來只是單純的環境設置問題,而且你這段程式碼看起來有改善的空間,我晚點來看看能不能簡化他。 anyway 如下

import asyncio

async def run_once():
    print('message')
    await asyncio.sleep(5)
    await run_once()

loop = asyncio.get_event_loop()
loop.run_until_complete(run_once())
loop.close()

說明一下:你上述的 code 沒用到 coroutine 的特性,跟直接用 sleep + callback 是差不多的。

所以 asyncio 到底好不好

Async programming 需要上手,但它是很有意義的工具,上手之後可以對付很多問題。

沒有什麼東西是不需要學習時間的。

asyncio 不好用,但它的設計沒有大問題,只是需要更多更好的包裝,以及合適的輔助工具。

所以你的意思是他雖然沒有設計上大問題,但因為 API 並不適合給 end user 使用,所以不好用囉?

官方的 asyncio 文件寫得很爛,但它其實不是給一般使用者看的;對一個完整的系統而言,文件本來也就不是最好的學習方法,與其把 asyncio 想成 Python 裡面的一個 module,更應該想成像 Django 這樣的完整 framework,需要 tutorial 與 cookbook,而目前還沒有足夠資源,所以學起來累。

如果你是一個 established user(但沒學過 concurrency 相關知識),想開始在你的程式裡加上一點這類功能,那麽 asyncio 不是好選擇。但如果你確定需求,且願意(希望)投資源下去,那麼 asyncio 是類似工具裡,最需要學習的一個。

asyncio 是有點不好,但都不是根本的缺陷,可以在未來改進。但更重要的事,它是對你的應用不見得好。

如果不好用、不適合,能探究原因會比跟從 best practice 好。我認為這才是工程師能不能更上一層樓的關鍵。

Summary of this reply:

總結一下我同意的觀點:

  1. Document 不夠清楚簡單、教學導向文件太少,所以不好上手
  2. asyncio 設計上不要求簡化(Task/Future/coroutine 容易誤用問題)

跟我不同意的觀點:

  1. async-await 在 python 以 multi thread/process 為主要思考面向

    • 如同你說的,asyncio 試圖整合不同類型的 concurrent 環境,否為 multi thread/process 應該不是重點,況且除了 JS 以外也有非 single thread 而使用 async-await,所以這論述並沒有說服力。
  2. generator 不夠用,所以需要 async syntax

    • 我想我附帶的參考文件應該就足夠說明,不過如果你需要範例的話,我應該可以花時間寫出你要的「async 的 generator」。
  3. 「別的語言,論述不變」沒有道理

    • 在 generator 或是 async 的觀點上,在現今眾多語言都支援 first-class function 甚至 async-await 語法糖支援,在我們提到的這些語言中,都沒有你所謂的「考慮不一樣的 interoperability」的疑慮。

以及可以再討論的:

  1. Async Programming 不符合人類習慣思維,不好上手
    • 真要說的話 ooxx paradigm programming language 本來就不是所有人的思考方式,我覺得這必須要有系統地做調查才能定論,不過這應該算是一種大型社會實驗。XD

補充一點,async-await 可以說是 asynchronous only generator,他們的重點都是如何用看起來「主動的」方式,來拿到原本只能「被動」取得的值,所以不管是哪種程式語言、是否為多執行緒、使用的封裝物件為 Promise/Task/Future 等等等等,都不影響他的抽象性質。

有什麼錯誤或是沒寫清楚的地方,再煩請你指教,謝謝 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment