Kotlin協(xié)程教程(1):?jiǎn)?dòng)

協(xié)程

協(xié)程簡(jiǎn)單的來說,就是用戶態(tài)的線程。

emmm,還是不明白對(duì)吧,那想象一個(gè)這樣的場(chǎng)景,如果在一個(gè)單核的機(jī)器上有兩個(gè)線程需要執(zhí)行,因?yàn)橐淮沃荒軋?zhí)行一個(gè)線程里面的代碼,那么就會(huì)出現(xiàn)線程切換的情況,一會(huì)需要執(zhí)行一下線程A,一會(huì)需要執(zhí)行一下線程B,線程切換會(huì)帶來一些開銷。

假設(shè)兩個(gè)線程,交替執(zhí)行,如下圖所示


Image.png

線程會(huì)因?yàn)門hread.sleep方法而進(jìn)入阻塞狀態(tài)(就是什么也不會(huì)執(zhí)行),這樣多浪費(fèi)資源啊。

能不能將代碼塊打包成一個(gè)個(gè)小小的可執(zhí)行片段,由一個(gè)統(tǒng)一的分配器去分配到線程上去執(zhí)行呢,如果我的代碼塊里要求sleep一會(huì),那么就去執(zhí)行別的代碼塊,等會(huì)再來執(zhí)行我呢。


Image [2].png

協(xié)程就是這樣一個(gè)東西,我們作為使用者不需要再去考慮創(chuàng)建一個(gè)新線程去執(zhí)行一坨代碼,也不需要關(guān)心線程怎么管理。我們需要關(guān)心的是,我要異步的執(zhí)行一坨代碼,待會(huì)我要拿到它的結(jié)果,我要異步的執(zhí)行很多坨代碼,待會(huì)我要按某種順序,或者某種邏輯得到它們的結(jié)果。

總而言之,協(xié)程是用戶態(tài)的線程,它是在用戶態(tài)實(shí)現(xiàn)的一套機(jī)制,可以避免線程切換帶來的開銷,可以高效的利用線程的資源。

從代碼上來講,也可以更漂亮的寫各種異步邏輯。

這里想再講講一個(gè)概念,阻塞與非阻塞是什么意思

阻塞與非阻塞

簡(jiǎn)單來說,阻塞就是不執(zhí)行了,非阻塞就是一直在執(zhí)行。
比如

Thread.wait() // 阻塞了
// 這里執(zhí)行不到了

但是,如果

while (true) { // 一直在運(yùn)行,沒有阻塞
   i++;
}
// 這里也執(zhí)行不到了

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

runBlocking是啟動(dòng)新協(xié)程的一種方法。

runBlocking啟動(dòng)一個(gè)新的協(xié)程,并阻塞它的調(diào)用線程,直到里面的代碼執(zhí)行完畢。

舉個(gè)例子

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,這并沒有什么稀奇,所有的代碼都在主線程執(zhí)行,按照順序來,去掉runBlocking也是一樣的嘛。

但是,runBlocking可以指定參數(shù),就可以讓runBlocking里面的代碼在其他線程執(zhí)行,但同樣可以阻塞外部線程。

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添加了一個(gè)參數(shù),Dispatchers.IO,這樣里面的代碼塊就會(huì)執(zhí)行到其他線程了。

來一起看看效果:

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

通過斷點(diǎn)在runBlocking里面的代碼,查看這個(gè)時(shí)候,主線程是什么狀態(tài),發(fā)現(xiàn)它是進(jìn)入了WAIT態(tài)。


Image [3].png

當(dāng)給runBlocking指定Dispatchers參數(shù)時(shí),就仿佛是使用了join方法。

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

t.join()

launch:?jiǎn)?dòng)一個(gè)協(xié)程

launch可以啟動(dòng)一個(gè)協(xié)程,但不會(huì)阻塞調(diào)用線程,但是launch必須要在協(xié)程作用域中才能調(diào)用。

fun main() {

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

如果要在非協(xié)程作用域調(diào)用launch,可以使用GlobalScope.launch。

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

同樣的launch也是可以傳入一個(gè)Dispatcher參數(shù)來指定它會(huì)被分配到什么線程上執(zhí)行。

此時(shí),大家就會(huì)想了,GlobalScope.launch那么方便,是不是只用它就行了?什么時(shí)候該用launch,什么時(shí)候該用GlobalScope.launch呢?

文檔這樣說道:GlobalScope.launch會(huì)啟動(dòng)一個(gè)top-level的協(xié)程,它的生命周期將只受到整個(gè)應(yīng)用程序生命周期的限制。

emmmm,那是不是說,普通的launch,它所創(chuàng)建的協(xié)程會(huì)受到外層的一個(gè)作用域的生命周期的影響,而GlobalScope所創(chuàng)建的協(xié)程,不收外層的影響。

于是,有了下面的實(shí)驗(yàn)

fun main() {

    runBlocking(Dispatchers.IO) {

        val job = launch { // 外層任務(wù),包裹兩個(gè)協(xié)程

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

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

        delay(300); // 延遲一會(huì),讓第二個(gè)協(xié)程能執(zhí)行3次左右

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

        delay(2000) // 繼續(xù)延遲,期望看到GlobalScope能繼續(xù)運(yùn)行
        
    }
}

看看實(shí)驗(yàn)結(jié)果

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 -----

如我的預(yù)料一樣,GlobalScope無法被cancel。

再來看一下文檔里面怎么描述的,體會(huì)一下:

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

接下來,解釋一下上面提到的協(xié)程作用域的概念。

什么是協(xié)程作用域(Coroutine Scope)?

協(xié)程作用域是協(xié)程運(yùn)行的作用范圍,換句話說,如果這個(gè)作用域銷毀了,那么里面的協(xié)程也隨之失效。就好比變量的作用域。

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

協(xié)程作用域也是這樣一個(gè)作用,可以用來確保里面的協(xié)程都有一個(gè)作用域的限制。

一個(gè)經(jīng)典的示例就是,比如我們要在Android上使用協(xié)程,但是我們不希望Activity銷毀了,我的協(xié)程還在悄咪咪的干一些事情,我希望它能停止掉。

我們就可以

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

這樣,里面運(yùn)行的協(xié)程就會(huì)隨著Activity的銷毀而銷毀。

launch的返回值:Job

回到launch的話題,launch啟動(dòng)后,會(huì)返回一個(gè)Job對(duì)象,表示這個(gè)啟動(dòng)的協(xié)程,我們可以方便的通過這個(gè)Job對(duì)象,取消,等待這個(gè)協(xié)程。

像這樣:

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方法,來等待這個(gè)協(xié)程執(zhí)行完畢。這個(gè)和Thread的join方法語義一樣。

async:?jiǎn)?dòng)協(xié)程的另一種姿勢(shì)

launch啟動(dòng)一個(gè)協(xié)程后,會(huì)返回一個(gè)Job對(duì)象,這個(gè)Job對(duì)象不含有任何數(shù)據(jù),它只是表示啟動(dòng)的協(xié)程本身,我們可以通過這個(gè)Job對(duì)象來對(duì)協(xié)程進(jìn)行控制。

假設(shè)這樣一種場(chǎng)景,我需要同時(shí)啟動(dòng)兩個(gè)協(xié)程來搞點(diǎn)事,然后它們分別都會(huì)計(jì)算出一個(gè)Int值,當(dāng)兩個(gè)協(xié)程都做完了之后,我需要將這兩個(gè)Int值加在一起并輸出。

如果使用launch,我們可能要在外層建立一個(gè)變量來記錄協(xié)程的輸出數(shù)據(jù)了,但是使用async,就可以輕松的解決這個(gè)問題!

async的返回值依然是個(gè)Job對(duì)象,但它可以帶上返回值。

上面的小需求可以用下面的代碼實(shí)現(xiàn):

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方法來獲取返回值,它會(huì)等待協(xié)程執(zhí)行完畢,并將返回值吐出來。

這樣上面的代碼就是兩個(gè)協(xié)程自己吭哧吭哧弄完之后,各自返回了10和20,外層再將它們加起來。

總結(jié)

這篇文章,我大概的講了一下協(xié)程的概念和被發(fā)明的初衷,以及在kotlin中,啟動(dòng)協(xié)程的基本方法,最后再總結(jié)一下,方便快速?gòu)?fù)習(xí)。

進(jìn)程是一個(gè)應(yīng)用程序的資源管理單元,線程是一個(gè)執(zhí)行單元,但當(dāng)線程這個(gè)執(zhí)行單元需要切換狀態(tài),停止,啟動(dòng),或者大量啟動(dòng)的時(shí)候,就會(huì)比較消耗資源。我們需要一個(gè)更輕巧,更容易被控制的執(zhí)行單元,這就是協(xié)程啦。

本篇介紹了runBlocking方法,它可以在非協(xié)程作用域下創(chuàng)建一個(gè)協(xié)程作用域,它的名字也很好,阻塞的執(zhí)行,意味著,它會(huì)阻塞它的調(diào)用線程,直到它內(nèi)部都執(zhí)行完畢。

launch和async都可以在協(xié)程作用域下啟動(dòng)協(xié)程,launch以Job對(duì)象的形式返回協(xié)程任務(wù)本身,可以通過Job來操作協(xié)程,async以Deferred對(duì)象的形式返回協(xié)程任務(wù),可以獲取執(zhí)行流的返回值。

GlobalScope.launch會(huì)創(chuàng)建一個(gè)頂層的協(xié)程,它只受限于整個(gè)應(yīng)用的生命周期,不建議使用。

相關(guān)閱讀


如果你喜歡這篇文章,歡迎點(diǎn)贊評(píng)論打賞
更多干貨內(nèi)容,歡迎關(guān)注我的公眾號(hào):好奇碼農(nóng)君

所有文章二維碼推廣圖_v2.png

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,360評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,123評(píng)論 0 286
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,559評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,742評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評(píng)論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,981評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評(píng)論 1 280
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,354評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,707評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容