Kotlin協程教程(1):啓動

協程

協程簡單的來講,就是用戶態的線程。java

emmm,仍是不明白對吧,那想象一個這樣的場景,若是在一個單核的機器上有兩個線程須要執行,由於一次只能執行一個線程裏面的代碼,那麼就會出現線程切換的狀況,一會須要執行一下線程A,一會須要執行一下線程B,線程切換會帶來一些開銷。app

假設兩個線程,交替執行,以下圖所示
圖片描述異步

線程會由於Thread.sleep方法而進入阻塞狀態(就是什麼也不會執行),這樣多浪費資源啊。async

能不能將代碼塊打包成一個個小小的可執行片斷,由一個統一的分配器去分配到線程上去執行呢,若是個人代碼塊裏要求sleep一會,那麼就去執行別的代碼塊,等會再來執行我呢。
圖片描述spa

協程就是這樣一個東西,咱們做爲使用者不須要再去考慮建立一個新線程去執行一坨代碼,也不須要關心線程怎麼管理。咱們須要關心的是,我要異步的執行一坨代碼,待會我要拿到它的結果,我要異步的執行不少坨代碼,待會我要按某種順序,或者某種邏輯獲得它們的結果。線程

總而言之,協程是用戶態的線程,它是在用戶態實現的一套機制,能夠避免線程切換帶來的開銷,能夠高效的利用線程的資源。code

從代碼上來說,也能夠更漂亮的寫各類異步邏輯。orm

這裏想再講講一個概念,阻塞與非阻塞是什麼意思協程

阻塞與非阻塞

簡單來講,阻塞就是不執行了,非阻塞就是一直在執行。
好比對象

Thread.wait() // 阻塞了
// 這裏執行不到了

可是,若是

while (true) { // 一直在運行,沒有阻塞
   i++;
}
// 這裏也執行不到了

runBlocking:鏈接阻塞與非阻塞的世界

runBlocking是啓動新協程的一種方法。

runBlocking啓動一個新的協程,並阻塞它的調用線程,直到裏面的代碼執行完畢。

舉個例子

println("aaaaaaaaa ${Thread.currentThread().name}")

runBlocking {
    for (i in 0..10) {
        println("$i ${Thread.currentThread().name}")
        delay(100)
    }
}

println("bbbbbbbbb ${Thread.currentThread().name}")

上面代碼的輸出爲:

aaaaaaaaa main
0 main
1 main
2 main
3 main
4 main
5 main
6 main
7 main
8 main
9 main
10 main
bbbbbbbbb main

emmm,這並無什麼稀奇,全部的代碼都在主線程執行,按照順序來,去掉runBlocking也是同樣的嘛。

可是,runBlocking能夠指定參數,就可讓runBlocking裏面的代碼在其餘線程執行,但一樣能夠阻塞外部線程。

println("aaaaaaaaa ${Thread.currentThread().name}")

runBlocking(Dispatchers.IO) { // 注意這裏
    for (i in 0..10) {
        println("$i ${Thread.currentThread().name}")
        delay(100)
    }
}

println("bbbbbbbbb ${Thread.currentThread().name}")

上面的代碼,給runBlocking添加了一個參數,Dispatchers.IO,這樣裏面的代碼塊就會執行到其餘線程了。

來一塊兒看看效果:

aaaaaaaaa main
0 DefaultDispatcher-worker-1
1 DefaultDispatcher-worker-1
2 DefaultDispatcher-worker-1
3 DefaultDispatcher-worker-4
4 DefaultDispatcher-worker-4
5 DefaultDispatcher-worker-6
6 DefaultDispatcher-worker-7
7 DefaultDispatcher-worker-7
8 DefaultDispatcher-worker-9
9 DefaultDispatcher-worker-1
10 DefaultDispatcher-worker-5
bbbbbbbbb main

經過斷點在runBlocking裏面的代碼,查看這個時候,主線程是什麼狀態,發現它是進入了WAIT態。
圖片描述

當給runBlocking指定Dispatchers參數時,就彷彿是使用了join方法。

val t = thread {
    for (i in 0..10) {
        println("$i ${Thread.currentThread().name}")
        Thread.sleep(100)
    }
}

t.join()

launch:啓動一個協程

launch能夠啓動一個協程,但不會阻塞調用線程,可是launch必需要在協程做用域中才能調用。

fun main() {

    launch {
        // no, no, no...
    }
    
    runBlocking {
        launch {
            // is ok
        }
    }
}

若是要在非協程做用域調用launch,可使用GlobalScope.launch。

fun main() {
    GlobalScope.launch {
        // is ok
    }
}

一樣的launch也是能夠傳入一個Dispatcher參數來指定它會被分配到什麼線程上執行。

此時,你們就會想了,GlobalScope.launch那麼方便,是否是隻用它就好了?何時該用launch,何時該用GlobalScope.launch呢?

文檔這樣說道:GlobalScope.launch會啓動一個top-level的協程,它的生命週期將只受到整個應用程序生命週期的限制。

emmmm,那是否是說,普通的launch,它所建立的協程會受到外層的一個做用域的生命週期的影響,而GlobalScope所建立的協程,不收外層的影響。

因而,有了下面的實驗

fun main() {

    runBlocking(Dispatchers.IO) {

        val job = launch { // 外層任務,包裹兩個協程

            GlobalScope.launch { // 第一個協程
                for (i in 0..10) {
                    println("GlobalScope $i ${Thread.currentThread().name} -----")
                    delay(100)
                }
            }

            launch { // 第二個協程
                for (i in 0..10) {
                    println("normal launch $i ${Thread.currentThread().name} #####")
                    delay(100)
                }
            }
        }

        delay(300); // 延遲一會,讓第二個協程能執行3次左右

        job.cancel() // 將外層任務取消了

        delay(2000) // 繼續延遲,指望看到GlobalScope能繼續運行
        
    }
}

看看實驗結果

GlobalScope 0 DefaultDispatcher-worker-2 -----
normal launch 0 DefaultDispatcher-worker-5 #####
GlobalScope 1 DefaultDispatcher-worker-5 -----
normal launch 1 DefaultDispatcher-worker-1 #####
GlobalScope 2 DefaultDispatcher-worker-5 -----
normal launch 2 DefaultDispatcher-worker-3 #####
GlobalScope 3 DefaultDispatcher-worker-7 -----
GlobalScope 4 DefaultDispatcher-worker-8 -----
GlobalScope 5 DefaultDispatcher-worker-8 -----
GlobalScope 6 DefaultDispatcher-worker-7 -----
GlobalScope 7 DefaultDispatcher-worker-1 -----
GlobalScope 8 DefaultDispatcher-worker-3 -----
GlobalScope 9 DefaultDispatcher-worker-9 -----
GlobalScope 10 DefaultDispatcher-worker-5 -----

如個人預料同樣,GlobalScope沒法被cancel。

再來看一下文檔裏面怎麼描述的,體會一下:

Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
and are not cancelled prematurely.

接下來,解釋一下上面提到的協程做用域的概念。

什麼是協程做用域(Coroutine Scope)?

協程做用域是協程運行的做用範圍,換句話說,若是這個做用域銷燬了,那麼裏面的協程也隨之失效。就比如變量的做用域。

{ // scope start
    int a = 100;
} // scope end
println(a); // what is a?

協程做用域也是這樣一個做用,能夠用來確保裏面的協程都有一個做用域的限制。

一個經典的示例就是,好比咱們要在Android上使用協程,可是咱們不但願Activity銷燬了,個人協程還在悄咪咪的幹一些事情,我但願它能中止掉。

咱們就能夠

class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    // ....
}

這樣,裏面運行的協程就會隨着Activity的銷燬而銷燬。

launch的返回值:Job

回到launch的話題,launch啓動後,會返回一個Job對象,表示這個啓動的協程,咱們能夠方便的經過這個Job對象,取消,等待這個協程。

像這樣:

fun main() {

    runBlocking(Dispatchers.IO) {

        val job1 = launch {
            for (i in 0..10) {
                println("normal launch $i ${Thread.currentThread().name} #####")
                delay(100)
            }
        }

        val job2 = launch {
            for (i in 0..10) {
                println("normal launch $i ${Thread.currentThread().name} -----")
                delay(100)
            }
        }

        job1.join()
        job2.join()

        println("all job finished")
    }
}

使用job的join方法,來等待這個協程執行完畢。這個和Thread的join方法語義同樣。

async:啓動協程的另外一種姿式

launch啓動一個協程後,會返回一個Job對象,這個Job對象不含有任何數據,它只是表示啓動的協程自己,咱們能夠經過這個Job對象來對協程進行控制。

假設這樣一種場景,我須要同時啓動兩個協程來搞點事,而後它們分別都會計算出一個Int值,當兩個協程都作完了以後,我須要將這兩個Int值加在一塊兒並輸出。

若是使用launch,咱們可能要在外層創建一個變量來記錄協程的輸出數據了,可是使用async,就能夠輕鬆的解決這個問題!

async的返回值依然是個Job對象,但它能夠帶上返回值。

上面的小需求能夠用下面的代碼實現:

fun main() {

    runBlocking(Dispatchers.IO) {

        val job1 = async {
            for (i in 0..10) {
                println("normal launch $i ${Thread.currentThread().name} #####")
                delay(100)
            }
            10 // 注意這裏的返回值
        }

        val job2 = async {
            for (i in 0..10) {
                println("normal launch $i ${Thread.currentThread().name} -----")
                delay(100)
            }
            20 // 注意這裏的返回值
        }

        println(job1.await() + job2.await())

        println("all job finished")
    }
}

這裏使用了await方法來獲取返回值,它會等待協程執行完畢,並將返回值吐出來。

這樣上面的代碼就是兩個協程本身吭哧吭哧弄完以後,各自返回了10和20,外層再將它們加起來。

總結

這篇文章,我大概的講了一下協程的概念和被髮明的初衷,以及在kotlin中,啓動協程的基本方法,最後再總結一下,方便快速複習。

進程是一個應用程序的資源管理單元,線程是一個執行單元,但當線程這個執行單元須要切換狀態,中止,啓動,或者大量啓動的時候,就會比較消耗資源。咱們須要一個更輕巧,更容易被控制的執行單元,這就是協程啦。

本篇介紹了runBlocking方法,它能夠在非協程做用域下建立一個協程做用域,它的名字也很好,阻塞的執行,意味着,它會阻塞它的調用線程,直到它內部都執行完畢。

launch和async均可以在協程做用域下啓動協程,launch以Job對象的形式返回協程任務自己,能夠經過Job來操做協程,async以Deferred對象的形式返回協程任務,能夠獲取執行流的返回值。

GlobalScope.launch會建立一個頂層的協程,它只受限於整個應用的生命週期,不建議使用。


若是你喜歡這篇文章,歡迎點贊評論打賞
更多幹貨內容,歡迎關注個人公衆號:好奇碼農君
圖片描述

相關文章
相關標籤/搜索