[說明] 原文中有一些範例程式說明,在此譯文中全部都省略啦!文章標題也沒有全然由原文直譯過來,然後有些很專業的東西,我看不懂的地方可能會有翻譯錯誤、或小小 bypass 過的情況。如果有發現錯誤之處,歡迎隨時指正,也能矯正我的一些盲點,感謝大家!
[原文] http://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/
*********************************** 文章開始 ***********************************
我曾在不同的地方碰到 "sync vs. async" callback 的問題,這也是一個會困擾 API 設計者與 API使用者的實際問題。
最近,我剛好在 Hammersmith 上碰到這樣的問題,它是用於 MongoDB 的 callback-based Scala API。我想這對很多撰寫 JVM 程式的人而言,是個新的考量,因為傳統上 JVM 使用阻塞式 (blocking) 的 APIs 跟 threads。
對於撰寫基於事件循環(event loop) client-side 程式的我,非同步的考量還挺眼熟的。
定義 (Definitions)
- 一支同步的 callback (synchronous callback) 會在函式 returns 之前被調用,即,該 API 所收到的 callback 與函式在同一塊堆疊(stack)上
- 一個例子像是:list.foreach(callback),當 foreach() 傳回時,你會預期每一個元素已經丟入 callback 中執行完畢了
- 一支非同步(asynchronous) 或推遲(deferred) 的 callback,在函式回傳之後才會被調用 (或至少是在另一個 thread 的 stack 中被調用)。推遲的機制包含 threads 與 main loops (其他的名稱包含 event loops, dispatchers, executors)
- 非同步的 callbacks 在 IO-related APIs 很常見到,如 socket.connect(callback),當 connect() 傳回時,你會預期它的 callback 可能還未被調用,因為它正等待著 connection 完成
方針 (Guidelines)
對於 callback-based API 的設計,我使用兩個基於我過去經驗的規則
- 一支 callback 應該總是 sync 或總是 async,並在 API 文件中載明此契約
- 一支 async callback 應該直接被 main loop 或集中分派機制所調用
同步與非同步 Callbacks 有何不同
對於應用程式開發者以及函式庫實作,sync 與 async callbacks 會引發不同的問題。
- Synchronous callbacks
- 在同樣的執行緒中被調用,因此,不會有 thread-safety 的疑慮
- 在像是 C/C++ 的語言中,可能會存取儲存在堆疊中的資料,像是本地變數
- 對任何語言,他們可能會存取跟當下執行緒相關的資料,像 thread-local 變數。例如許多 Java web frameworks 會為當前的交易(transaction)或請求(request)建立 thread-local 變數
- 有時候能假設應用程式的某些狀態不變,例如假設物件的存在、計時器還未被觸發、沒有發生IO、或是任何跟程式結構相關的狀態
- Asynchronous callbacks
- 可能在另一個執行緒被調用 (for thread-based deferral mechanisms),因此 apps 一定要同步那些 callback 會存取到的任何資源
- 無法觸及原有 stack 或 thread 的資料,像 local 變數或 thread-local 資料
- 如果原有的 thread 握有 locks,callback 會在它們之外被調用
- 一定要假設其它 threads 或 events 可能修改了應用程式的狀態
沒有說哪一種 callback 比較好,因為它們各有用途
看看下面這條程式碼。大多情況下,如果這支函式的 callback 被推遲了或在當前的執行緒沒有做任何事,你一定會感到非常的訝異list.foreach(callback)
但如果像以下這條,它的 callback 若沒有被推遲,就形同於完全沒有意義,那麼還需要 callback 幹嘛?
socket.connect(callback)
這兩個例子顯示出,為什麼給定一支 callback 需要定義它是 sync 或 async;這兩者是不可以互換的,並且本來就有不同的用途。
只能在 SYNC 或 ASYNC 選擇其中一種
需要立即調用 callback (如,資料已經可用) 的情況跟需要將 callback 推遲 (例如socket 還沒 ready) 的情況都不算罕見。有一個做法很吸引人,那就是當可以的時候,就立即同步地調用 callback,否則就推遲(defer)它。不過,這可不是個好主意。(譯者: 我就用過這樣的爛主意~~泣~)
因為 sync 跟 async callbacks 有不同的規則,它們會產生不同的 bugs。要應用程式開發者同時為 sync 跟 async 的情況進行規劃與測試真的太困難了,這一點在函式庫內部很容易解決:如果 callback 本來就有機會被推遲,那麼就總是推遲它
同步的資源應該推遲所有它們會調用的 Callbacks
這條規則是:函式庫應該在調用一支 application callback 之前,放開它持有的所有鎖。放開所有鎖最簡單的方法是令 callback 為 async,把它推遲到堆疊捲回 main loop 後執行、或是在另一個 thread 的堆疊執行它。
這很重要,因為你不能預期應用程式會自己避免在 callback 中接觸到你的 API。如果在你握有 locks 的同時 app 接觸了你的 API,那 app 就會打死結(deadlock) (如果你是用 recursive locks,你將會遇到很可怕的正確性問題(correctness problem))
若不使用非同步 callback,同步資源也可以嘗試先鬆開它的所有鎖,但是這可能會非常痛苦,你要將 callback 往回傳遞給堆疊最外層的 lock holder,讓 holder 放掉鎖之後再執行 callback~ 啊~~
結論
因為要將 callback 的執行給推遲實在是太重要了,如果你有事件迴圈可以用的話,callback-based APIs 會工作的很棒,這也是為什麼 callbacks 在 client-side JavaScript、node.js、GTK+ 中能工作的非常好的原因。但是,如果你要在 JVM 上實作 callback-based APIs,能不能工作得很好,就沒有正解了。你需要選擇一些事件迴圈的 library 來用(Akka 工作的很棒),或自己做一個事件迴圈。
因為 callback-based APIs 目前很流行,如果你打算寫這樣的 APIs,我想這篇文章會是不錯的開始。
*********************************** 文章結束 ***********************************
*********************************** 文章結束 ***********************************