Skip to content

Instantly share code, notes, and snippets.

@uranusjr
Last active June 15, 2018 07:35
Show Gist options
  • Save uranusjr/93f26913a5239034c50aa6b8ccac7a07 to your computer and use it in GitHub Desktop.
Save uranusjr/93f26913a5239034c50aa6b8ccac7a07 to your computer and use it in GitHub Desktop.

asyncio 好寫嗎?

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

1. Async Program 好寫嗎?

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

2. Async-await 好用嗎?

看你跟誰比。正常人都比較習慣 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 這些東西容易理解得多,也更容易上手。

3. asyncio 好用嗎?

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

這和 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 的 generators 沒辦法為 Python 的所有功能實作 async variant,這會有雞生蛋生雞問題。最簡單的矛盾:如果你只用 generator 語法實作 async API,那要怎麼實作 async 的 generator?

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

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

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

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

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

這裡面還有很多很多講不完的雷(例如為什麼 ensure_future 隱含產生的 task 被放入 event loop),不過上面應該夠證明 async 的實作標準不是所有語言通用了。

所以 asyncio 到底好不好

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

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

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

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

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

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