Kotlin協程開篇

《Kotlin協程》均基於Kotlinx-coroutines 1.3.70

· 協程是什麼?

· 什麼時候用協程?

· 協程的核心是什麼?

· kotlin的協程和其他語言的協程有什麼異同?

協程沒那麼難

協程的出現是爲了解決異步編程中遇到的各種問題。從高級編程語言出現的第一天,異步執行的問題就伴隨出現。

在Kotlin裏使用協程非常方便,

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在後臺啓動一個新的協程並繼續
        delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
        println("World!") // 在延遲後打印輸出
    }
    println("Hello,") // 協程已在等待時主線程還在繼續
    Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
}

上面的代碼是一個常規啓動協程的方式,關鍵函數只有 launch,delay,這兩個函數是kotlin協程獨有的。其他函數都屬於基本庫。

代碼的輸出結果是

Hello,
World!

這是一個典型的異步執行結果,先得到 Hello,而不是按代碼順序先得到 World。

異步執行在平時開發中經常遇到,比如執行一段IO操作,不管是文件讀寫,還是網絡請求,都屬於IO。

在Android中我們對IO操作的一個熟知的規則是不能寫在主線程中,因爲它會卡線程,導致ANR。而上面的代碼其實是不會卡線程的。
用同步的方式寫異步代碼
這句話在很多資料中出現過,劃重點。

理解這句話的關鍵在於,協程幹了什麼,讓這個異步操作不會卡主線程?

我們知道類似的技術在RxJava中也有,它通過手動切線程的方式指定代碼運行所在的線程,從而達到不卡主線程的目的。而協程的高明和簡潔之處在於,開發者不需要主動切線程。

在上面的代碼中打印一下線程名觀察結果。

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在後臺啓動一個新的協程並繼續
        println("Thread: ${Thread.currentThread().name}")
        delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
        println("World!") // 在延遲後打印輸出
    }
    println("Thread: ${Thread.currentThread().name}")
    println("Hello,") // 協程已在等待時主線程還在繼續
    Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
}

我們會得到

Thread: main
Hello,
Thread: DefaultDispatcher-worker-1
World!

可以看到在打印World的時候,代碼是運行在子線程的。

協程其實沒那麼容易

對於經常用協程開發的人來說,有幾個很有意思的問題值得思考下。
· 上面代碼中的Thread.sleep()可以改成delay()嗎?

· 爲什麼理論上可以開無限多個coroutine?

· 假設有一個IO操作 foo() 耗時a,一個計算密集操作 bar() 耗時b,用協程來執行的話,launc{a b} 耗時c,c是否等於a + b?

另外一個很有意思的問題需要用代碼來展示。在協程中有一個函數 runBlocking{},沒接觸過的可以簡單理解爲它等價於launch{}。

用它來改造上面的代碼,

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking


fun main() = runBlocking {
    GlobalScope.launch { // 在後臺啓動一個新的協程並繼續
        println("Thread: ${Thread.currentThread().name}")
        delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
        println("World!") // 在延遲後打印輸出
    }
    println("Thread: ${Thread.currentThread().name}")
    println("Hello,") // 協程已在等待時主線程還在繼續
    Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
}

我們會得到

Thread: DefaultDispatcher-worker-1
Thread: main
Hello,
World!

現在我們把 GlobalScope.launch這行改造一下,

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    launch { // 在後臺啓動一個新的協程並繼續
        println("Thread: ${Thread.currentThread().name}")
        delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
        println("World!") // 在延遲後打印輸出
    }
    println("Thread: ${Thread.currentThread().name}")
    println("Hello,") // 協程已在等待時主線程還在繼續
    Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
}

現在再看執行結果,

Thread: main
Hello,
Thread: main
World!

WTF? launch裏的代碼也執行在主線程了?

這個問題涉及到Kotlin協程的Scope,調度,也是協程的實現核心邏輯

Kotlin不是第一個提出協程的

實際上在Kotlin之前就有不少語言實踐了協程這個概念。比如python,golang。

而最原始的協程其實不叫協程,叫纖程(Fiber)。聽說過Fiber的人都已經。。

甲:聽說過纖程嗎
乙:Fiber是吧
甲:你今年起碼40歲了吧

纖程是微軟第一個提出的,但因爲它的使用非常的反人類,對程序員的代碼質量要求非常高,以至於沒人願意用它。雖然現在還可以在微軟官網上找到關於纖程的資料,但能用好纖程的程序員鳳毛麟角。

Using Fibers

直到golang的出現,才把協程這個技術發揚光大。有人說python也有協程呀,爲什麼是golang。
其實python的協程不是真正意義上的協程,後面我們會說到。python的協程是基於yield關鍵字進行二次封裝的,雖然在高層抽象上也是以函數作爲協程粒度,但對比golang差的太遠。

golang做了什麼

golang的協程叫goroutine,跟kotlin的coroutine差不多。golang用一種程序員更容易理解的抽象定義了協程粒度goroutine,還有它的各種操作。

對於程序員來說,再也不用關心什麼時候切協程,協程在什麼線程運行這種問題,開發效率和代碼運行效率得到成倍提升。

golang在編譯器上做了很多優化,當代碼中發生IO或者內核中斷的時候,會自動幫你切協程。熟悉計算機原理的能明白,當發生內核中斷的時候,比如請求一個磁盤文件,中斷髮生時CPU其實是沒有工作的,執行邏輯在這個時候處於一個空轉,直到中斷返回結果才繼續往下執行。

於是在中斷髮生的時候,CPU相當於浪費了一段時間。golang在這個時候切協程,就能把CPU浪費的算力利用起來交給另外一個協程去執行。

kotlin的協程還在發展

如果去看kotlin的協程源碼的話會發現裏面有很多 exeprimental 的api和實現邏輯。直到1.3.70爲止,jetbain團隊還在繼續地爲coroutine機制增加新的活力。目前來說coroutine處於一個穩定階段,可以基於1.3.70版本來分析它,後面應該不會有很大機制上的變動了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章