首先,其實還滿開心有機會可以做個小小辯論的啦。我的回應如下
這把好幾個主題混在一起了,需要一個一個看。
不好寫。東西好不好寫的意思,代表它和 programmer 的內在思考模式符合程度。Async program 不符合人類習慣思維,所以才會被說一開始的學習 overhead 很高,寫起來容易卡。尤其如果同步與異步 paradigms 並立(例如 Python),就更容易在切換的時候出問題。
一開始學習的 overhead 很高不代表寫起來就會很卡,舉例來說不少人覺得 Rails 寫起來很順手,但大部分的人都說相對學習門檻比較高。我的確有一段時期不習慣 async program,但現在是完全沒有問題的。當然這只能代表我個人經驗,不過同時也是我要問的問題:你覺得 Async Program 不好寫,所以是哪裡不好寫?你的回答是「不符合人類習慣思維」。
看你跟誰比。正常人都比較習慣 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,不過我們在討論的是易用,而不是效能極限。
不好用。它的內部架構有些地方很繞(例如我就不懂為什麼
Future
與Task
要是兩個不同的類型),介面也不 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 有它能做和不能做的事。它是
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
這沒有道理,因為每個語言(和內建函式庫)都有自己的特性,把一樣的東西架上去時,就需要考慮不一樣的 interoperability。Python 人和 C# 人重疊性很低,然後大部分人都不太熟 Lua(我也不熟),所以就用 JavaScript 當例子和 Python 比較吧。在 JavaScript 實作 Promise/A+ 與 Python 實作
asyncio
底層時的差異可以分成兩部分來討論:
同意,當然每個語言都有不同的特性,比較時的確需要考慮,這也是為什麼我要問這些問題。
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 更容易撰寫的論述。
因為 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 是差不多的。
Async programming 需要上手,但它是很有意義的工具,上手之後可以對付很多問題。
沒有什麼東西是不需要學習時間的。
asyncio
不好用,但它的設計沒有大問題,只是需要更多更好的包裝,以及合適的輔助工具。
所以你的意思是他雖然沒有設計上大問題,但因為 API 並不適合給 end user 使用,所以不好用囉?
官方的
asyncio
文件寫得很爛,但它其實不是給一般使用者看的;對一個完整的系統而言,文件本來也就不是最好的學習方法,與其把asyncio
想成 Python 裡面的一個 module,更應該想成像 Django 這樣的完整 framework,需要 tutorial 與 cookbook,而目前還沒有足夠資源,所以學起來累。
如果你是一個 established user(但沒學過 concurrency 相關知識),想開始在你的程式裡加上一點這類功能,那麽
asyncio
不是好選擇。但如果你確定需求,且願意(希望)投資源下去,那麼asyncio
是類似工具裡,最需要學習的一個。
asyncio
是有點不好,但都不是根本的缺陷,可以在未來改進。但更重要的事,它是對你的應用不見得好。
如果不好用、不適合,能探究原因會比跟從 best practice 好。我認為這才是工程師能不能更上一層樓的關鍵。
總結一下我同意的觀點:
- Document 不夠清楚簡單、教學導向文件太少,所以不好上手
- asyncio 設計上不要求簡化(Task/Future/coroutine 容易誤用問題)
跟我不同意的觀點:
-
async-await 在 python 以 multi thread/process 為主要思考面向
- 如同你說的,asyncio 試圖整合不同類型的 concurrent 環境,否為 multi thread/process 應該不是重點,況且除了 JS 以外也有非 single thread 而使用 async-await,所以這論述並沒有說服力。
-
generator 不夠用,所以需要 async syntax
- 我想我附帶的參考文件應該就足夠說明,不過如果你需要範例的話,我應該可以花時間寫出你要的「async 的 generator」。
-
「別的語言,論述不變」沒有道理
- 在 generator 或是 async 的觀點上,在現今眾多語言都支援 first-class function 甚至 async-await 語法糖支援,在我們提到的這些語言中,都沒有你所謂的「考慮不一樣的 interoperability」的疑慮。
以及可以再討論的:
- Async Programming 不符合人類習慣思維,不好上手
- 真要說的話 ooxx paradigm programming language 本來就不是所有人的思考方式,我覺得這必須要有系統地做調查才能定論,不過這應該算是一種大型社會實驗。XD
補充一點,async-await 可以說是 asynchronous only generator,他們的重點都是如何用看起來「主動的」方式,來拿到原本只能「被動」取得的值,所以不管是哪種程式語言、是否為多執行緒、使用的封裝物件為 Promise/Task/Future 等等等等,都不影響他的抽象性質。
有什麼錯誤或是沒寫清楚的地方,再煩請你指教,謝謝 :)