Simply Patrick

Kotlin Coroutines and Threads

Coroutine 跟 thread 的關係

Coroutine 通常可以理解成輕量化的 thread,但實際運作還是需要被排程到作業系統層級的 thread,然後再被排程到某個 CPU來執行:

相對於 thread,使用 coroutine 的好處是, coroutine 之間的切換快速,需要耗用的系統資源也比較小。

Coroutine 如何排程?

Kotlin 的 coroutine 會被分派到那個 thread 是由呼叫 coroutine builder 時提供的 CoroutineContext 參數來決定。

透過觀察這個範例的輸出,我們可以了解不同 CoroutineContext 的行為:

class CoroutineContextActivity : ConsoleActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        logThread("Begin onCreate")

        launch(UI) {
            logThread("Begin launch(UI)")
            delay(10_000)
            logThread("End launch(UI)")
        }

        launch(CommonPool) {
            logThread("Begin launch(CommonPool)")
            delay(10_000)
            logThread("End launch(CommonPool)")
        }

        launch(Unconfined) {
            logThread("Begin launch(Unconfined)")
            delay(10_000)
            logThread("End launch(Unconfined)")
        }

        launch(newSingleThreadContext("MyOwnThread")) {
            logThread("Begin launch(newSingleThreadContext)")
            delay(10_000)
            logThread("End launch(newSingleThreadContext)")
        }

        logThread("End onCreate")
    }

    fun logThread(msg: String) {
        println("$msg: ${Thread.currentThread().name}")
    }
}

結果:

1:48:50:866 (   1) Begin onCreate: main
1:48:50:937 (   1) Begin launch(Unconfined): main
1:48:50:938 (1719) Begin launch(CommonPool): ForkJoinPool.commonPool-worker-2
1:48:50:967 (   1) End onCreate: main
1:48:50:968 (1721) Begin launch(newSingleThreadContext): MyOwnThread
1:48:51:015 (   1) Begin launch(UI): main
1:49:00:971 (1721) End launch(newSingleThreadContext): MyOwnThread
1:49:00:973 (1720) End launch(Unconfined): kotlinx.coroutines.DefaultExecutor
1:49:00:980 (1724) End launch(CommonPool): ForkJoinPool.commonPool-worker-1
1:49:01:016 (   1) End launch(UI): main

心得:

  • Unconfined 在 suspend 之前會是由 calling thread 直接執行,suspend 之後則是用 DefaultExecutor thread 來執行。
  • CommonPool 會是由 common thread pool 裡面的 thread 來執行,suspend 前後被分配的 thread 有可能不同。
  • UI 只能在 main thread 上執行,並且因為是依賴 Android Looper 來實現的,coroutine 會在 onCreate 結束後才被執行到。
  • newSingleThreadContext 會由新產生的 thread 來執行,因爲創建新的 thread 需要一點時間,coroutine 是在 onCreate 結束後才被呼叫到。

這樣的設計主要有兩個優點:一來提供彈性讓使用者根據使用情境決定要使用那些 thread,二來提供擴充性讓使用者可以設計不一樣的 thread dispatching strategy。

如果想要掌握更進階的 coroutine 用法,Coroutine context and dispatchers 是很不錯的使用指南。


Kotlin Coroutine API

Kotlin Coroutine 的架構

Kotlin 從 1.1 版開始實驗性地支援 coroutine,主要的目標是簡化非同步編程的複雜度。Diving deep into Kotlin Coroutines 解釋了 coroutine 跟一般常用的 callback 及目前蠻多人在用的 reactive 模式有何不同。

由於目前 kotlinx-coroutines-core 的實現包含了不同抽象程度的 API,初學者建議可以用下面這張圖來學習如何使用 Kotlin coroutine:

範例分析

套用下面這個範例來理解:

suspend fun sendEmail(r: String, msg: String): Boolean { // 3
    delay(2000) // 4
    println("Sent '$msg' to $r")
    return true
}

suspend fun getReceiverAddressFromDatabase(): String { // 3
    delay(1000) // 4
    return "[email protected]"
}

suspend fun sendEmailSuspending(): Boolean { // 3
    val msg = /* 1 */ async(CommonPool) { // 2            
        delay(500)
        "The message content"
    }
    val recipient = /* 1 */ async(CommonPool) { // 2
        getReceiverAddressFromDatabase()  
    } 
    println("Waiting for email data")
    val sendStatus = /* 1 */ async(CommonPool) { // 2
        sendEmail(recipient.await(), msg.await()) // 4
    }
    return sendStatus.await() 
}

fun main(args: Array<String>) = /* 1 */ runBlocking(CommonPool) { 
    val job = /* 1 */ launch(CommonPool) { // 2 
        sendEmailSuspending() 
        println("Email sent successfully.")
    }
    job.join() // 4
    println("Finished")
}

其中:

  1. runBlocking, launch, 及 async 是最常被使用的 coroutine builder。
  2. coroutine builder 後面跟著的就是我們撰寫的 suspending lambda,可以視為 coroutine 的進入點。
  3. 通常在 suspending lambda 裡會再呼叫其他 suspending function,例如 sendEmailSuspending
  4. delay, Job.join, Deferred.await 則是用來等待的 high-level API。

High-level API

範例中的 async/await 並不是目前唯一支援的 concurrency pattern,目前看到正在開發中的還有:

Low-level API

所有的 coroutine 機制都是基於底層的 low-level API 來實現的。之後有時間的話會再研究並分享 high-level API 是如何用 low-level API 來實現的。


Devdocs Desktop

寫(code)之前先讀(API)

當一個軟體開發者,需要花蠻多時間來閱讀文檔以便了解如何使用某個 API 以及各種情況下它會表現的行為,因此學習如何有效率地查詢及閱讀了解 API 是蠻重要的能力。基本上如果某個 library 是你工作上常用到的,我會建議你從頭到尾把所有文件至少讀一遍,而不是需要用到時才去查。

Dash 好用,但是…

在 macOS 上,我過去是習慣使用 Dash 這工具來查詢 API,一是它整合了蠻多常用的 API 文檔,二來它查詢的速度還不錯,就個人使用上算是很好的投資,所以當時就在 App Store 買了正式版。然而好景不常,當 Dash 3 出來後,2.x 版本就常發生文件顯示不出來的問題,讓我有點惱火:angry:,這樣的品質再加上發生 被 App Store 下架 的事件,要我再付錢買 3.x 版本是不可能的事。

Zeal 是 Linux 或 Windows 上不錯的選擇

之前找到比較好的替代方案是 Zeal, 但是 Zeal 的 docset 是 Dash 提供的,道義上不能跟 Dash 打對台,因此官方並不提供 macOS 的版本。建議的解決方案是用 Wine 在 macOS 上執行 Windows 版本的 Zeal,我試過是可行的,搞定一些小問題後運作上還算正常。

devdocs.io 更棒

另一個更好的選擇是 devdocs.io,無須安裝直接網頁打開就可查詢。想要 desktop 版本? 沒問題! 這種網站最適合用 Electron 來包了: DevDocs Desktop。這是我目前最推薦的 API 文檔工具,跨 macOS, Linux, Windows 都可以使用且免費,太佛心了。


Better Way to Request Runtime Permissions