本文收錄于 kotlin入門潛修專題系列,歡迎學(xué)習(xí)交流拭宁。
創(chuàng)作不易,如有轉(zhuǎn)載瓣俯,還請(qǐng)備注杰标。
協(xié)程
從本篇文章開(kāi)始,將會(huì)開(kāi)啟kotlin的另一個(gè)世界——協(xié)程彩匕,這是kotlin提供的異步處理機(jī)制腔剂。
提到異步,自然而然想起的就是多線程驼仪、多進(jìn)程掸犬,這是繞不開(kāi)的話題,但是本系列文章將暫時(shí)不闡述他們的區(qū)別绪爸,而是先將協(xié)程相關(guān)的知識(shí)闡述完畢后湾碎,在來(lái)做一個(gè)整體的對(duì)比以及剖析協(xié)程背后的原理。
本篇文章將會(huì)演示協(xié)程的基本使用方法奠货。
Hello World介褥!
本小節(jié)先來(lái)看一個(gè)協(xié)程的“hello world”。
首先递惋,kotlin協(xié)程相關(guān)的接口是位于 kotlinx.coroutines包下的柔滔,因此,我們?cè)谑褂脜f(xié)程的時(shí)候萍虽,要首先導(dǎo)入該包睛廊。但是該包并不位于kotlin標(biāo)準(zhǔn)庫(kù)中(kotlin竟然沒(méi)有提供標(biāo)準(zhǔn)庫(kù)的協(xié)程支持),而是作為獨(dú)立的庫(kù)發(fā)布的杉编,因此我們需要先引入?yún)f(xié)程相關(guān)的依賴超全。
在這里我們采用的是maven引入的方式,如下所示:
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.1.1</version>
</dependency>
需要注意王财,kotlin的版本一定要在版本1.3以上才可以使用卵迂,因?yàn)?.2版本中的協(xié)程還是實(shí)驗(yàn)性質(zhì)的,所以推薦使用最新的kotlin版本(對(duì)于idea可以升級(jí)kotlin插件绒净,如果無(wú)法升級(jí)則需要升級(jí)idea至最新版本)见咒,這樣才能用到相對(duì)穩(wěn)定的協(xié)程相關(guān)的接口方法。
如果是使用gradle作為構(gòu)建工具挂疆,則可以像下面一樣引入依賴:
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
}
這兩種基本已經(jīng)滿足我們的使用需求了(maven常用于java工程改览,gradle則常用于android工程)下翎,其他構(gòu)建方式就不再闡述。
下面來(lái)看下kotlin協(xié)程之“hello world”示例宝当,如下所示:
fun main(args: Array<String>) {
GlobalScope.launch {
delay(1000L)
println("world!")
}
println("hello ")
Thread.sleep(2000L)
}
上面的代碼執(zhí)行完成后视事,打印結(jié)果如下所示:
hello
world!
GlobalScope.launch表示啟動(dòng)一個(gè)后臺(tái)協(xié)程,該協(xié)程的任務(wù)是延時(shí)1000ms后打印“world!”字符庆揩。當(dāng)kotlin啟動(dòng)過(guò)該協(xié)程后俐东,會(huì)繼續(xù)執(zhí)行其后面的代碼,所以我們看到先打印出了“hello ”字符串订晌。需要注意虏辫,我們?cè)诖蛴ello字符串語(yǔ)句之后又調(diào)用了Thread.sleep方法,并延時(shí)等待了2000ms(理論上只要稍微大于GlobalScope.launch中的延時(shí)時(shí)間即可)锈拨,這么做的原因是保持jvm繼續(xù)存活砌庄,否則程序執(zhí)行完后(即main方法線程執(zhí)行完后)就會(huì)被終止,而不會(huì)執(zhí)行GlobalScope.launch中延時(shí)后的打印語(yǔ)句(后面會(huì)有新的機(jī)制可以不必這么做)奕枢,這點(diǎn)和多線程不一樣娄昆。此外,采用GlobalScope啟動(dòng)的協(xié)程(它是top-level級(jí)別的協(xié)程)缝彬,其生命周期將會(huì)伴隨整個(gè)應(yīng)用程序的生命周期萌焰。
再來(lái)看一下delay方法,該方法的聲明如下所示:
suspend fun delay(timeMillis: Long):Unit
該方法的意思是跌造,延遲協(xié)程的執(zhí)行杆怕,延遲時(shí)間由timeMillis決定,表示延遲的毫秒數(shù)壳贪。delay方法并不會(huì)阻塞線程陵珍,而僅僅會(huì)掛起當(dāng)前協(xié)程,并在其指定延遲的時(shí)間到達(dá)時(shí)喚醒當(dāng)前協(xié)程违施。
由上面代碼可知互纯,delay方法使用了suspend關(guān)鍵字進(jìn)行修飾,故名思議磕蒲,suspend修飾的方法表示該方法是個(gè)可掛起方法留潦,只能用于協(xié)程中或者被suspend方法調(diào)用。
上面示例中辣往,實(shí)際上用到了java中的Thread線程機(jī)制(用于阻塞main線程)兔院,其實(shí)kotlin本身也提供了一種機(jī)制來(lái)阻塞線程,如下所示:
fun main(args: Array<String>) {
GlobalScope.launch {
delay(1000L)
println("world!")
}
println("hello ")
runBlocking {
delay(2000L)
}
}
上面代碼執(zhí)行結(jié)果同樣會(huì)打印“hello world!”站削,runBlocking的作用就是用于阻塞main線程坊萝,以保持jvm的存活(否則程序運(yùn)行結(jié)束后會(huì)被回收)。注意,前面說(shuō)過(guò)delay方法并不阻塞線程十偶,所以這里不要誤以為是delay方法在阻塞main線程菩鲜,其背后的機(jī)制實(shí)際上是,main線程會(huì)等待runBlocking中的代碼執(zhí)行完成惦积!
我們也可以結(jié)合runBlocking來(lái)采用多協(xié)程的方式完成上述功能接校,如下所示:
fun main(args: Array<String>) = runBlocking {
GlobalScope.launch {
delay(1000L)
println("world!")
}
println("hello ")
delay(2000L)
}
打印結(jié)果同上。這里需要注意狮崩,我們使用runBlocking方法啟動(dòng)了main協(xié)程蛛勉,然后又在main協(xié)程中啟動(dòng)了一個(gè)新的后臺(tái)協(xié)程(即GlobalScope.launch ),因?yàn)閙ain協(xié)程中的delay方法運(yùn)行在runBlocking方法中厉亏,所以這種寫(xiě)法董习,依然能起到“阻塞main線程”的目的(main線程需要等待runBlocking運(yùn)行完成)。
runBlocking<Unit>中的Unit表示main方法的返回值爱只,這里只不過(guò)是進(jìn)行了顯示指定,其默認(rèn)返回值其實(shí)就是Unit招刹。
使用delay方法可以起到延遲等待的作用恬试,但是這顯然不是一個(gè)好的實(shí)現(xiàn)方式,畢竟程序執(zhí)行的時(shí)間有些時(shí)候并不是確定的疯暑。如果我們想實(shí)現(xiàn)健全的等待機(jī)制训柴,可以利用kotlin為我們提供的job,如下所示:
fun main(args: Array<String>) = runBlocking {
val job = GlobalScope.launch {
delay(1000L)
println("world!")
}
println("hello ")
job.join()//這里會(huì)等待協(xié)程job執(zhí)行完成
}
上面job的類型是kotlin為我們提供的接口Job妇拯,表示一個(gè)任務(wù)幻馁。后面文章會(huì)進(jìn)行闡述。
結(jié)構(gòu)化并發(fā)
在kotlin中越锈,我們使用GlobalScope.launch啟動(dòng)的協(xié)程是top-level級(jí)別仗嗦,雖然協(xié)程是輕量級(jí)的,但是依然會(huì)消耗資源甘凭。比如我們啟動(dòng)了很多協(xié)程稀拐,在運(yùn)行的時(shí)候發(fā)生了未知錯(cuò)誤,如果沒(méi)有一個(gè)健全的機(jī)制回收這些資源丹弱,那么kotlin是不會(huì)自動(dòng)幫我們處理的德撬,針對(duì)這種情況,我們可以使用結(jié)構(gòu)化并發(fā)躲胳。即像使用線程一樣蜓洪,在需要的時(shí)候創(chuàng)建,而不是創(chuàng)建top-level級(jí)別的協(xié)程坯苹,如下所示:
fun main(args: Array<String>) = runBlocking {
launch {
delay(500L)
println("world!")
}
println("hello ")
}
上面代碼同樣會(huì)打印'hello world'隆檀,與前面不同的是,我們沒(méi)有再讓main協(xié)程等待一定的時(shí)間,也沒(méi)有使用join機(jī)制刚操。這是因?yàn)檎⒊幔琹aunch創(chuàng)建的協(xié)程,其作用域?qū)儆趓unBlocking(參見(jiàn)下節(jié)作用域構(gòu)造器)菊霜,此時(shí)坚冀,runBlocking會(huì)等待在作用域內(nèi)啟動(dòng)的所有協(xié)程的執(zhí)行,直到他們?nèi)客瓿刹艜?huì)結(jié)束鉴逞。
對(duì)于上面的代碼记某,我們還可以結(jié)合前面提到的suspend方法做進(jìn)一步整理,那就是將launch中的語(yǔ)句抽象出來(lái)构捡,作為一個(gè)單獨(dú)的方法液南,如下所示:
fun main(args: Array<String>) = runBlocking {
launch {
printWorld()
}
println("hello ")
}
//這里必須定義為suspend方法,否則無(wú)法使用同時(shí)suspend的delay方法勾徽。
suspend fun printWorld() {
delay(500L)
println("world!")
}
作用域構(gòu)造器(Scope builder)
上面提到的協(xié)程作用域滑凉,實(shí)際上就是由不同的作用域構(gòu)造器決定的,除了使用系統(tǒng)提供的默認(rèn)作用域構(gòu)造器喘帚,我們同樣可以自己定義作用域構(gòu)造器畅姊。kotlin為我們提供了創(chuàng)建作用域構(gòu)造器的方法:coroutineScope。使用該方法會(huì)創(chuàng)建一個(gè)新的協(xié)程作用域吹由,coroutineScope和runBlocking很相似若未,他們都會(huì)等待其作用域內(nèi)的所有協(xié)程執(zhí)行完成后才會(huì)結(jié)束,但是coroutineScope不會(huì)像runBlocking那樣阻塞線程倾鲫。來(lái)看個(gè)例子:
fun main(args: Array<String>) = runBlocking {
launch {//launch屬于runBlocking作用域
delay(200L)
println("Task from runBlocking")
}
//創(chuàng)建了一個(gè)新的協(xié)程作用域粗合,屬于runBlocking作用域
coroutineScope {
launch {//屬于coroutineScope作用域
delay(300L)
println("Task from nested launch in coroutine scope")
}
delay(100L)
println("Task from coroutine scope")
}
//屬于runBlocking作用域,該打印語(yǔ)句永遠(yuǎn)會(huì)在coroutineScope
//返回之后執(zhí)行乌昔,但是如果和開(kāi)始處的launch方法中的打印時(shí)機(jī)進(jìn)行對(duì)比
//則取決于launch的delay時(shí)間
println("coroutine scope is over")
}
我們先分析下上面的代碼:
首先隙疚,我們使用runBlocking創(chuàng)建了一個(gè)main協(xié)程,在其代碼塊中玫荣,我們使用launch啟動(dòng)了一個(gè)新協(xié)程甚淡,該協(xié)程作用域?qū)儆趍ain協(xié)程,launch啟動(dòng)的協(xié)程執(zhí)行時(shí)機(jī)將會(huì)有其內(nèi)部的delay時(shí)間決定捅厂。
接著贯卦,我們使用coroutineScope創(chuàng)建了一個(gè)新的協(xié)程作用域,該協(xié)程會(huì)等待其作用域內(nèi)的所有協(xié)程執(zhí)行完成后才會(huì)結(jié)束焙贷。這意味著撵割,在這個(gè)示例中,只有coroutineScope內(nèi)部的所有協(xié)程執(zhí)行完成之后辙芍,coroutineScope才會(huì)返回啡彬,也就是說(shuō) println("coroutine scope is over")
這條語(yǔ)句永遠(yuǎn)都會(huì)在coroutineScope返回之后才執(zhí)行羹与。
最后,再結(jié)合不同的delay時(shí)間就不難推測(cè)出打印結(jié)果庶灿,如下所示:
Task from coroutine scope
Task from runBlocking
Task from nested launch in coroutine scope
coroutine scope is over
Global 協(xié)程
前面已經(jīng)多次提到了Global協(xié)程纵搁,這里在明確一下它的概念。
在kotlin中往踢,Global協(xié)程相當(dāng)于后臺(tái)線程的概念腾誉,即Gloabal協(xié)程并不會(huì)保證進(jìn)程的存活,應(yīng)用程序結(jié)束了它就結(jié)束了峻呕,來(lái)看個(gè)例子:
fun main(args: Array<String>) = runBlocking {
GlobalScope.launch {
repeat(100) { i ->
println(i)
delay(100L)
}
}
delay(300L)
println("end...")
}
上面代碼執(zhí)行過(guò)后打印如下:
0
1
2
3
end...
也就是說(shuō)利职,程序什么時(shí)候結(jié)束,取決于runBlocking方法delay的時(shí)間瘦癌,這也就是文章剛開(kāi)始的時(shí)候猪贪,我們使用Global協(xié)程,為什么要等待一定時(shí)間的原因讯私。當(dāng)然热押,看到這里,我們顯然有很多解決方法了(比如將GlobalScope.launch改為launch)斤寇,這里只是更進(jìn)一步的說(shuō)明一下GlobalScope協(xié)程的機(jī)制楞黄。
至此,本篇文章闡述完畢抡驼。