CPS 與 Kotlin coroutine

Continuation Passing Style

在異步編程中,因爲沒法拿到實時的結果,咱們每每會經過設置回調的方式來處理執行的結果。編程

fun doSomethingAsync(param1: Int, param2: Any, callback: (Any?) -> Unit) {
    // ...
    // when execution is done
    callback.invoke(result)
}
複製代碼

假設咱們約定一種編程規範,全部的函數都按照上述的方式來定義,即全部的函數都直接返回結果值,而是在參數列表最後的位置傳入一個 callback 函數參數,並在函數執行完成時經過這個 callback 來處理結果,這種代碼風格被稱爲延續傳遞風格(Continuation Passing Style)。這裏的回調函數(callback)被稱爲是延續(continuation),即這個回調函數決定了程序接下來的行爲,整個程序的邏輯就是經過一個個的延續而拼接在一塊兒。異步

In functional programming, continuation-passing style (CPS) is a style of programming in which control is passed explicitly in the form of a continuation. (from Wikipedia.)async

CPS 的優勢

咱們在實現異步邏輯的時候會天然而然的採用相似 CPS 的方式,這是由於咱們不知道何時能夠處理方法的結果,因此把控制邏輯傳給了要調用的方法,讓該方法本身在執行完成後去主動調用。咱們本來必須遵照順序執行的控制邏輯,可是 CPS 給了咱們一個機會能夠去自定義控制邏輯。那麼自定義控制邏輯能夠作哪些事情呢?讓咱們來看一個例子。ide

構建單線程事件循環模型

咱們來增長一點規則:每次調用函數並傳入 callback 後,先將 callback 轉換成 EventCallback。EventCallback 會將 callback 放入一個單線程線程池中去執行,示例代碼以下所示。函數式編程

val singleThreadExecutor = Executors.newSingleThreadExecutor()

fun ((Any?) -> Unit).toEventCallback(): ((Any?) -> Unit) {
    return fun(result: Any?) {
        singleThreadExecutor.submit {
            this.invoke(result)
        }
    }
}

fun doSomething(param1: Any, callback: (Any?) -> Unit) {
    var result: Any? = null
    // ...
    // when execution is done
    callback.toEventCallback().invoke(result)
}
複製代碼

對於一些須要耗時等待的操做(例如 IO 操做),咱們能夠定義一些特殊的函數,在這些函數裏具體邏輯被放到一個特定的線程池中去執行,待操做完成後再返回事件線程,這樣能夠保證咱們的事件線程不被阻塞。異步編程

val IOThreadPool = Executors.newCachedThreadPool()

fun doSomethingWithIO(param1: Any, callback: (Any?) -> Unit) {
    IOThreadPool.submit {
        var result: Any? = null
        // ...
        // when io operation is done
        callback.toEventCallback().invoke(result)
    }
}
複製代碼

這樣咱們實際創建了一個與 Node.js 相似的單事件循環+異步IO的執行模型,能夠看到經過使用 CPS 的方式咱們能夠更靈活的處理返回值,例如選擇恰當的時機或者是作攔截操做。函數

CPS 的缺點

Callback Hell

在普通的執行模型中,若是咱們須要多個前提值來計算一個最終結果,那麼咱們只須要順序計算每一個值,而後在計算結果,每一個前提值的計算過程都是平級的,可是在 CPS 中,執行順序是經過回調傳遞的,因此咱們不得不每一個值的計算做爲一個回調嵌套到另外一個值的計算過程當中,這就是所謂的 Callback Hell,這樣的代碼會致使難以閱讀。oop

// Normal
val a = calculateA()
val b = calculateB()
val c = calculateC()
// ...
val result = calculateResult(a, b, c/*, ...*/)

// CPS
fun calculateResult(callback: (Any?) -> Unit) {
    calculateA(fun(a: Any?) {
        calculateB(fun(b: Any?) {
            calculateC(fun(c: Any?) {
                //...
                callback.invoke(calculate(a, b, c/*, ...*/)
            }
        }
    }
}
複製代碼

棧空間佔用問題

在相似 C 和 Java 這樣的語言裏,每次函數調用會爲該函數分配對應的棧空間,用來存放函數參數,返回值和局部變量的信息,而後在函數返回以後再釋放這部分空間。而在 CPS 模型中,咱們能夠看到,回調是在函數執行完成前被調用的,因此在進入回調函數以後外面的函數的棧空間並不會被釋放,這樣程序很容易出現棧空間溢出的問題。優化

CPS 中的回調其實具備一些特殊性,即老是做爲函數執行的最後一個步驟(代替普通流程中的返回值),因此這個時候外層函數的值並不會再被訪問,這種狀況實際上是尾遞歸調用的一種表現。在絕大多數的函數式語言中,系統都會對尾遞歸進行優化,回收外層函數的棧空間。可是在 C 和 Java 中並無這樣的優化。this

Kotlin coroutine

Kotlin coroutine 本質上就是利用 CPS 來實現對過程的控制,並解決了一些用 CPS 時會產生的問題。

suspend 關鍵字

Kotlin 中 suspend 函數的寫法與普通函數基本一致,可是編譯器會對標有 suspend 關鍵字的函數作 CPS 變換,這解決了咱們提到的 callback hell 的問題:咱們依然能夠按照普通的順序執行流程來寫代碼,而編譯器會自動將其變爲 CPS 的等價形式。

另外,爲了不棧空間過大的問題,kotlin 編譯器實際上並無把代碼轉換成函數回調的形式,而是利用了狀態機模型。Kotlin 把每次調用 suspend 函數的地方稱爲一個 suspension point,在作編譯期 CPS 變換的時候,每兩個 suspension point 之間能夠視爲一個狀態,每次進入狀態機的時候會有一個當前的狀態,而後會執行該狀態對應的代碼,若是這時程序執行完畢,則返回結果值,不然返回一個特殊的標記,表示從這個狀態退出並等待下次進入。這樣至關於實現了一個可複用的回調,每次都使用這個回調而後根據狀態的不一樣執行不一樣的代碼。

流程控制

同咱們上面控制回調執行的例子同樣,kotlin 也能夠對 suspend 函數進行控制,實現的方式是經過 CoroutineContext 類。在每一個 suspend 函數執行的地方都會有一個對應的 CoroutineContext,CoroutineContext 是一個相似單向鏈表的結構,系統回去遍歷這個鏈表並根據每一個元素對 suspend 函數執行的流程進行控制,例如咱們能夠經過 Dispatcher 類控制函數執行的線程,或者經過 Job 類來 cancel 當前的函數執行。咱們可使用 coroutine 來重寫一下咱們上面定義的模型:

class SingleLoopEnv: CoroutineScope {
		
override val coroutineContext: CoroutineContext = 
        Executors.newSingleThreadExecutor().asCoroutineDispatcher()

    suspend fun doSomething(param1: Any?): Any? {
        var result: Any? = null
        // ...
        // when execution is done
        return result
    }

    fun doSomethingWithIO(param1: Any?): Deferred<Any?> = 
            GlobalScope(Dispatchers.IO).async {
        var result: Any? = null
        // ...
        // when io operation is done
        return result
    }

    fun main() = launch {
        val result = doSomething(null)
        // handle result
        // ...

        val ioResult = doSomethingWithIO(null).await()
        // handle io result
        // ...
    }
}
複製代碼

總結

像 Kotlin 提供一些其它機制同樣,coroutine 其實也是一種語法糖,可是這是一種比較高級的語法糖,它改變了咱們代碼的執行邏輯,使得咱們能夠更好的利用 CPS 這一函數式編程的思想,去解決複雜的異步編程問題。

Article by Orab

相關文章
相關標籤/搜索