這把好幾個主題混在一起了。一個一個看。
不好寫。東西好不好寫,代表它和 programmer 內在思考模式符不符合。Async program 不符合人類習慣思維,才會被說一開始的學習 overhead 很高,寫起來容易卡。尤其如果同步異步 paradigms 並立(例如 Python),就更容易在切換的時候出問題。
看你跟誰比。正常人都比較習慣 synchronous programming,所以 async program 的寫法越 synchronous,感覺就會越好用。Function reference callback → Anonymous function callback → Promise API → Async-await 這整個系統本身就在把 async program 寫得越來越像 synchronous,當然越後面越好用。
但上面只考慮 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 這些東西容易理解得多,也更容易上手。
不好用。它內部架構有些地方很繞(例如我就不懂為什麼 Future
與 Task
要是兩個不同的類型),介面很多地方不一致(有時候只吃 coroutine 有時候只吃 future 大部分時候又都可以),不過更大的問題是對一般使用者而言暴露出太多底層。
這和 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 的 generators 沒辦法為 Python 的所有功能實作 async variant,這會有雞生蛋生雞問題。最簡單的矛盾:如果你只用 generator 語法實作 async API,那要怎麼實作 async 的 generator?
這沒有道理,因為每個語言(和內建函式庫)都有自己的特性,把一樣的東西架上去時,就需要考慮不一樣的 interoperability。Python 人和 C# 人重疊性很低,然後大部分人都不太熟 Lua(我也不熟),所以就用 JavaScript 當例子和 Python 比較吧。在 JavaScript 實作 Promise/A+ 與 Python 實作 asyncio
底層時的差異可以分成兩部分來討論:
隱含 event loop 是 JavaScript 的基本性質,整個 ecosystem 也圍繞著它發展。Python 沒有這種東西,所以函式庫有的不考慮 event loop,也有的實作自己的 event loop。前面講過 asyncio
的初始目的就是為了統一它們。但因為少了 JavaScript 的假設,實作與介面就必須非常 explicit,用起來就很卡。
因為 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
的 coroutine 沒辦法像 JavaScript 的 async function 那樣有透明的 API,可以直接與 promise API 對接。Coroutine 沒有關於 event loop 的資訊,不知道自己要怎麼執行;你必須用它建立一個 task(通常用 ensure_future
,至於為什麼名字是 future 建出來卻是 task 你問 Guido 吧我也很想知道),event loop 才能執行它。
這裡面還有很多很多講不完的雷(例如為什麼 ensure_future
隱含產生的 task 被放入 event loop),不過上面應該夠證明 async 的實作標準不是所有語言通用了。
Async programming 需要上手,但它是很有意義的工具,上手之後可以對付很多問題。
asyncio
不好用,但它的設計沒有大問題,只是需要更多更好的包裝,以及合適的輔助工具。
官方的 asyncio
文件寫得很爛,但它不是給一般使用者看的;對一個完整的系統而言,文件本來也就不是最好的學習方法,與其把 asyncio
想成 Python 裡面的一個 module,更應該想成像 Django 這樣的完整 framework,需要 tutorial 與 cookbook。目前還沒有足夠資源,所以學起來累。
如果你是一個 established user(但沒學過 concurrency 相關知識),想開始在你的程式裡加上一點這類功能,那麽 asyncio
不是好選擇。但如果你確定需求,且願意(希望)投資源下去,那麼 asyncio
是類似工具裡,最需要學習的一個。
asyncio
有點不好,但都不是根本的缺陷,可以未來再改進。但更重要的是,它對你的應用不見得好。