簡(jiǎn)介
隨著kotlin不斷普及神得,以其簡(jiǎn)潔的語(yǔ)法糖频轿,易擴(kuò)展,空安全痊银,汲取了不同語(yǔ)言的優(yōu)點(diǎn)等...越來(lái)越受到開發(fā)者的青睞抵蚊。剛?cè)?code>kotlin,除了和Java
不一樣的語(yǔ)法讓人難以習(xí)慣外溯革,“攜程”和“泛型”更是讓開發(fā)者頭疼贞绳。接下來(lái)由我?guī)Т蠹伊私鈑otlin攜程基本使用。
其它文章
【kotlin】- delay函數(shù)實(shí)現(xiàn)原理
【kotlin】- 攜程的執(zhí)行流程
【kotlin】- 攜程的掛起和恢復(fù)
創(chuàng)建攜程
-
如果在kotlin使用Thread創(chuàng)建線程致稀,還在像Java那樣new
一個(gè)Thread
對(duì)象冈闭,似乎缺乏違和感。kotlin提供了直接創(chuàng)建線程的方法抖单。fun main() { thread(start = true,isDaemon = false){ println("${treadName()}=====創(chuàng)建一個(gè)線程") } }
輸出:
Thread-0=====創(chuàng)建一個(gè)線程 Process finished with exit code 0
是不是比Java的方式要簡(jiǎn)便許多萎攒,
isDaemon
指定線程是否是守護(hù)線程
,如果這里指定為true
日志是打印不出來(lái)的喲矛绘,原因可以百度一下守護(hù)線程
耍休。 -
fun main() { // CoroutineScope(英文翻譯:攜程范圍,即我們的攜程體) GlobalScope.launch (CoroutineName("指定攜程名字")){ delay(1000) println("${Thread.currentThread().name}======全局?jǐn)y程~") } }
很簡(jiǎn)單的一個(gè)例子(官方例子
main
最后調(diào)用了sleep
延遲函數(shù))货矮,運(yùn)行main
羊精,發(fā)現(xiàn)在控制臺(tái)并沒有打印協(xié)調(diào)體中的日志,輸出如下:Process finished with exit code 0
使用官方例子
fun main() { GlobalScope.launch (CoroutineName("指定攜程名字")){ delay(1000) println("${Thread.currentThread().name}======全局?jǐn)y程~") } Thread.sleep(2000L) println("${Thread.currentThread().name}======我是最后的倔犟~") }
運(yùn)行輸出如下:
DefaultDispatcher-worker-1======全局?jǐn)y程~ main======我是最后的倔犟~ Process finished with exit code 0
解釋
從第打印圖可以看出囚玫,攜程創(chuàng)建了新的線程
DefaultDispatcher-worker-1
來(lái)執(zhí)行喧锦,不在主線程读规,所以全局?jǐn)y程體和全局?jǐn)y程體外的代碼是在不同線程中異步執(zhí)行的。
全局?jǐn)y程創(chuàng)建的是守護(hù)線程燃少,而主線程不是掖桦,所以當(dāng)進(jìn)程中所有非守護(hù)線程執(zhí)行完,進(jìn)程就會(huì)退出供汛,守護(hù)進(jìn)程也將不復(fù)存在。
這就是為什么上面例子不能執(zhí)行打印代碼的原因涌穆。守護(hù)線程
是指為其他線程服務(wù)的線程怔昨。在JVM中,所有非守護(hù)線程都執(zhí)行完畢后宿稀,無(wú)論有沒有守護(hù)線程趁舀,虛擬機(jī)都會(huì)自動(dòng)退出。因此祝沸,JVM退出時(shí)矮烹,不必關(guān)心守護(hù)線程是否已結(jié)束kotlin攜程創(chuàng)建的線程對(duì)象是
CoroutineScheduler中Worker內(nèi)部類
,看一下這個(gè)內(nèi)部類的初始化罩锐。默認(rèn)就是守護(hù)線程奉狈。如果大家想要驗(yàn)證,可以使用jps
打印當(dāng)前執(zhí)行的Java進(jìn)程涩惑,在用jstack
查看進(jìn)程中相關(guān)線程的情況仁期。internal inner class Worker private constructor() : Thread() { init {isDaemon = true} }
-
// runBlocking協(xié)程構(gòu)建器將 main 函數(shù)轉(zhuǎn)換為協(xié)程 fun main(): Unit = runBlocking { launch { delay(1000) println("${treadName()}======局部攜程~") } }
launch
是CoroutineScope的擴(kuò)展函數(shù),所以必須在攜程體內(nèi)才可以調(diào)用竭恬。輸出如下:main======局部攜程~ Process finished with exit code 0
runBlocking
會(huì)阻塞當(dāng)前線程并且等待跛蛋,在所有已啟動(dòng)的子協(xié)程執(zhí)行完畢之前不會(huì)結(jié)束。所以launch啟動(dòng)就是runBlocking子攜程痊硕,因?yàn)閘aunch在runBlocking攜程作用域中赊级。在看一個(gè)例子:fun main(): Unit = runBlocking { GlobalScope.launch { delay(2000L) println("${treadName()}======全局?jǐn)y程") } // 如果沒有下面的代碼,上面代碼不會(huì)執(zhí)行 launch { delay(1000L) println("${treadName()}======局部攜程") } }
輸出如下:
main======局部攜程 Process finished with exit code 0
解釋
GlobalScope.launch啟動(dòng)是全局?jǐn)y程岔绸,會(huì)重新新建一個(gè)線程來(lái)執(zhí)行理逊,并不是runBlocking的子攜程。所以并不會(huì)等待GlobalScope.launch攜程體執(zhí)行完再退出進(jìn)程亭螟。
-
suspend fun main() { // 聲明攜程作用域,掛起函數(shù),會(huì)釋放底層線程用于其他用途,創(chuàng)建一個(gè)協(xié)程作用域并且在所有已啟動(dòng)子協(xié)程執(zhí)行完畢之前不會(huì)結(jié)束 coroutineScope { // 在該攜程作用域啟動(dòng)攜程 launch { delay(3000L) println("${treadName()}======才開始學(xué)習(xí)coroutines") } } println("${treadName()}======最后的倔犟~") }
這種方式啟動(dòng)的攜程作用域就在
coroutineScope
內(nèi)挡鞍。注意日志線程名字
,輸出如下:DefaultDispatcher-worker-1======才開始學(xué)習(xí)coroutines DefaultDispatcher-worker-1======最后的倔犟~ Process finished with exit code 0
從日志發(fā)現(xiàn)预烙,
main
居然不上在主線程執(zhí)行的墨微,其實(shí)并不是這樣,反編譯kotlin代碼扁掸,發(fā)現(xiàn)main
主入口代碼變成這樣了RunSuspendKt.runSuspend(new KotlinShareKt$$$main(var0))
翘县。其實(shí)coroutineScope
就是創(chuàng)建一個(gè)攜程環(huán)境最域。在看一個(gè)復(fù)雜的點(diǎn)的例子
fun main() = runBlocking { launch { delay(2000L) println("${treadName()}======Task from runBlocking") } coroutineScope { // 創(chuàng)建一個(gè)協(xié)程作用域 launch { delay(1000L) println("${treadName()}======Task from nested launch") } delay(100L) println("${treadName()}======Task from coroutine scope") // 這一行會(huì)在內(nèi)嵌 launch 之前輸出 } println("${treadName()}======scope is over") }
輸出如下:
main======Task from coroutine scope main======Task from nested launch main======scope is over main======Task from runBlocking Process finished with exit code 0
解釋
launch {...}
執(zhí)行了掛起函數(shù)delay,而coroutineScope{...}
可以看著也是一個(gè)子攜程體锈麸,調(diào)用掛起函數(shù)delay镀脂。而launch {...}和coroutineScope{...}后面的代碼誰(shuí)先執(zhí)行就要看launch中delay延遲的時(shí)間了。 -
fun main() { val cs = CoroutineScope(Dispatchers.Default) cs.launch { } }
-
使用給定的協(xié)程上下文調(diào)用指定的掛起塊忘伞,掛起直到它完成薄翅,并返回結(jié)果fun main() = runBlocking { val result = withContext(Dispatchers.Default) { delay(3000) println("${treadName()}======1") 30 } println("${treadName()}======$result") }
輸出如下:
DefaultDispatcher-worker-1======1 main======30 Process finished with exit code 0
-
需要引入庫(kù)androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02
使用:
lifecycleScope.launch {}
-
在實(shí)踐中絕大多數(shù)取消一個(gè)協(xié)程的理由是它有可能超時(shí)。 當(dāng)你手動(dòng)追蹤一個(gè)相關(guān)Job
的引用并啟動(dòng)了一個(gè)單獨(dú)的協(xié)程在延遲后取消追蹤氓奈,這里已經(jīng)準(zhǔn)備好使用withTimeout
函數(shù)來(lái)做這件事翘魄。withTimeout(1300L) { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } }
擴(kuò)展
由于取消只是一個(gè)例外,所有的資源都使用常用的方法來(lái)關(guān)閉舀奶。 如果你需要做一些各類使用超時(shí)的特別的額外操作暑竟,可以使用類似withTimeout
的withTimeoutOrNull
函數(shù),并把這些會(huì)超時(shí)的代碼包裝在try {...} catch (e: TimeoutCancellationException) {...}
代碼塊中育勺,而withTimeoutOrNull
通過(guò)返回null
來(lái)進(jìn)行超時(shí)操作但荤,從而替代拋出一個(gè)異常。 -
suspend fun doSomethingUsefulOne(): Int { delay(1000L) // 假設(shè)我們?cè)谶@里做了一些有用的事 return 13 } suspend fun doSomethingUsefulTwo(): Int { delay(1000L) // 假設(shè)我們?cè)谶@里也做了一些有用的事 return 29 }
- 默認(rèn)順序調(diào)用
val time = measureTimeMillis { val one = doSomethingUsefulOne() val two = doSomethingUsefulTwo() println("The answer is ${one + two}") } println("Completed in $time ms")
- async 并發(fā)
val time = measureTimeMillis { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}") } println("Completed in $time ms")
- 惰性啟動(dòng)的 async
可選的涧至,async
可以通過(guò)將start
參數(shù)設(shè)置為CoroutineStart.LAZY
而變?yōu)槎栊缘摹?在這個(gè)模式下腹躁,只有結(jié)果通過(guò)await
獲取的時(shí)候協(xié)程才會(huì)啟動(dòng),或者在Job
的 start`函數(shù)調(diào)用的時(shí)候南蓬。val time = measureTimeMillis { val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() } val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() } // 執(zhí)行一些計(jì)算 one.start() // 啟動(dòng)第一個(gè) two.start() // 啟動(dòng)第二個(gè) println("The answer is ${one.await() + two.await()}") } println("Completed in $time ms")
- 默認(rèn)順序調(diào)用
-
依然例子先行:fun main() = runBlocking{ val job = GlobalScope.launch { // 啟動(dòng)一個(gè)新協(xié)程并保持對(duì)這個(gè)作業(yè)的引用 delay(1000L) println("World!") } println("Hello,") job.join() // 等待直到協(xié)程執(zhí)行結(jié)束 }
輸出如下:
Hello, World! Process finished with exit code 0
按照之前的講解潜慎,
GlobalScope.launch
啟動(dòng)的是全局?jǐn)y程,并不屬于runBlocking的子攜程蓖康,所以runBlocking不會(huì)等待該攜程執(zhí)行完畢再退出進(jìn)程铐炫,那為什么這里會(huì)等待呢,那這就是join函數(shù)的功勞蒜焊,join作用是掛起協(xié)程直到攜程執(zhí)行完成倒信。 -
協(xié)程的取消是 協(xié)作 的。一段協(xié)程代碼必須協(xié)作才能被取消泳梆。 所有kotlinx.coroutines
中的掛起函數(shù)都是 可被取消的 鳖悠。它們檢查協(xié)程的取消, 并在取消時(shí)拋出CancellationException
然而优妙,如果協(xié)程正在執(zhí)行計(jì)算任務(wù)乘综,并且沒有檢查取消的話,那么它是不能被取消的套硼。val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextPrintTime = startTime var i = 0 while (i < 5) { // 一個(gè)執(zhí)行計(jì)算的循環(huán)卡辰,只是為了占用 CPU // 每秒打印消息兩次 if (System.currentTimeMillis() >= nextPrintTime) { println("job: I'm sleeping ${i++} ...") nextPrintTime += 500L } } } delay(1300L) // 等待一段時(shí)間 println("main: I'm tired of waiting!") job.cancelAndJoin() // 取消一個(gè)作業(yè)并且等待它結(jié)束 println("main: Now I can quit.")
打印輸出并沒在控制臺(tái)上看到堆棧跟蹤信息的打印。這是因?yàn)樵诒蝗∠膮f(xié)程中
CancellationException
被認(rèn)為是協(xié)程執(zhí)行結(jié)束的正常原因 -
在 finally 中釋放資源
fun main() = runBlocking { val job = launch { try { repeat(1000) { i -> println("job: I'm sleeping $i ...") delay(500L) } } finally { println("job: I'm running finally") } } delay(1300L) // 延遲一段時(shí)間 println("main: I'm tired of waiting!") job.cancelAndJoin() // 取消該作業(yè)并且等待它結(jié)束 println("main: Now I can quit.") }
-
運(yùn)行不能取消的代碼塊
在前一個(gè)例子中任何嘗試在finally
塊中調(diào)用掛起函數(shù)的行為都會(huì)拋出CancellationException
,因?yàn)檫@里持續(xù)運(yùn)行的代碼是可以被取消的九妈。通常反砌,這并不是一個(gè)問(wèn)題,所有良好的關(guān)閉操作(關(guān)閉一個(gè)文件萌朱、取消一個(gè)作業(yè)宴树、或是關(guān)閉任何一種通信通道)通常都是非阻塞的,并且不會(huì)調(diào)用任何掛起函數(shù)晶疼。然而酒贬,在真實(shí)的案例中,當(dāng)你需要掛起一個(gè)被取消的協(xié)程翠霍,你可以將相應(yīng)的代碼包裝在withContext(NonCancellable) {……}
中同衣,并使用 'withContext'函數(shù)以及NonCancellable
上下文。val job = launch { try { repeat(1000) { i -> println("job: I'm sleeping $i ...") delay(500L) } } finally { withContext(NonCancellable) { println("job: I'm running finally") delay(1000L) println("job: And I've just delayed for 1 sec because I'm non-cancellable") } } } delay(1300L) // 延遲一段時(shí)間 println("main: I'm tired of waiting!") job.cancelAndJoin() // 取消該作業(yè)并等待它結(jié)束 println("main: Now I can quit.")
結(jié)束語(yǔ)
很多例子都是官網(wǎng)的壶运,只是加上一些自己的理解,這篇文章只是帶大家快速入門kotlin攜程使用浪秘,后面會(huì)逐步深入蒋情,講解攜程的實(shí)現(xiàn)原理。