Kotlin Coroutines太方便了, 但有時候Blocking一下也不錯
自從 Kotlin 正式推出以來,Coroutines 應該可以算是其中一個最吸引人的功能了。Kotlin Coroutines 讓開發者可以用同步處理的思維來做非同步處理,因此非同步的操作變得更加簡單了,可以說是典範轉移 (Paradigm Shift)的一個好例子。
雖然 Coroutines 的簡潔與強大的功能,大大地簡化了非同步處理所要注意的細節,但卻也可能帶來一些意想不到的問題,甚至要更謹慎地設計程式架構。接下來我會介紹自己遇到的一個實例來說明使用 Coroutines 的時候有哪些需要特別注意。
使用Coroutines處理檔案IO
前一陣子需要做一個功能是讀取某個目錄底下所有電子書檔案並且把封面擷取出來,其中 epub 跟 pdf 的處理是使用 open source library PdfBox 跟epublib。一開始很直覺地認為用 Coroutines 處理 IO 簡直完美。於是寫了一個 function 像這樣
目的是掃描某個目錄下的所有檔案,每個檔案都用一個 coroutine 來處理,而且每一個需要花時間處理的步驟也用不同的 coroutine。乍看之下似乎很美好,但是實際上測試的時候卻發現效能很差,而且很容易出現 Out Of Memory 的問題。那麼問題出在哪邊呢??
Coroutines 的 Scheduling
Kotlin Coroutines 不像 kernel thread 本身有OS 負責處理 Scheduling,而是根據 CoroutineDispatcher 的實作來決定 。在這例子中,所有的 coroutine 都用了 Dispatchers.IO
,而 Dispatchers.IO 把每個 coroutine 丟到內部的 queue 。
所以讀每個檔案都建立一個 coroutine 的話,像下面的 forEach
forEach {
scope.launch { readBooksFromPath(it) }
}
如果檔案有 1000個,等於會在 dispatcher的 queue放進 1000個以上的 coroutine,讀完檔案接下來的動作都會排在這1000個 coroutine 後面,加上還有處理 Bitmap,等同於開 1000張 Bitmap,這樣會有 OOM 實在是一點也不意外 。
Solutions
至於該怎麼解決這問題呢? 有好幾個方法。
- 新建一個 Dispatcher 讀檔案用,而不是用預設的 Dispatcher.IO
- 讀完檔案之後的動作不是 suspend function
- 不用
launch
, 改用runBlocking
- 其他
最後我是改用 runBlocking
的方式,有幾個原因。
- 同時讀多個檔案並沒有提高效率,因為使用的library看起來都不是stream-based的處理,反而是一個接一個的讀取檔案效率比較好
- 同時還有其他 Job 也使用 Dispatcher.IO
- 每一個檔案處理完之後就可以被 GC,不會有 OOM 的問題 (除非要處理的檔案真的超大)
- 修改簡單,而且最後也是要等所有檔案處理完的結果
結論
Kotlin Coroutine提供了相當強大而且安全的非同步處理,但是如果不了解其中的一些細節的話,很容易遇到意想不到的結果,尤其是同時產生大量 coroutine的時候,沒有特別注意的話,還是有可能造成 OOM 的問題。
各位有什麼 Kotlin Coroutines 使用上的心得也歡迎分享討論唷。
下次見