簡介
隨著kotlin不斷普及,以其簡潔的語法糖,易擴展,空安全,汲取了不同語言的優(yōu)點等...越來越受到開發(fā)者的青睞。剛?cè)?code>kotlin,除了和Java
不一樣的語法讓人難以習慣外,“攜程”和“泛型”更是讓開發(fā)者頭疼。接下來由我?guī)Т蠹伊私鈑otlin攜程基本使用。
其它文章
【kotlin】- delay函數(shù)實現(xiàn)原理
【kotlin】- 攜程的執(zhí)行流程
【kotlin】- 攜程的掛起和恢復
創(chuàng)建攜程
-
如果在kotlin使用Thread創(chuàng)建線程,還在像Java那樣new
一個Thread
對象,似乎缺乏違和感。kotlin提供了直接創(chuàng)建線程的方法。fun main() { thread(start = true,isDaemon = false){ println("${treadName()}=====創(chuàng)建一個線程") } }
輸出:
Thread-0=====創(chuàng)建一個線程 Process finished with exit code 0
是不是比Java的方式要簡便許多,
isDaemon
指定線程是否是守護線程
,如果這里指定為true
日志是打印不出來的喲,原因可以百度一下守護線程
。 -
fun main() { // CoroutineScope(英文翻譯:攜程范圍,即我們的攜程體) GlobalScope.launch (CoroutineName("指定攜程名字")){ delay(1000) println("${Thread.currentThread().name}======全局攜程~") } }
很簡單的一個例子(官方例子
main
最后調(diào)用了sleep
延遲函數(shù)),運行main
,發(fā)現(xiàn)在控制臺并沒有打印協(xié)調(diào)體中的日志,輸出如下:Process finished with exit code 0
使用官方例子
fun main() { GlobalScope.launch (CoroutineName("指定攜程名字")){ delay(1000) println("${Thread.currentThread().name}======全局攜程~") } Thread.sleep(2000L) println("${Thread.currentThread().name}======我是最后的倔犟~") }
運行輸出如下:
DefaultDispatcher-worker-1======全局攜程~ main======我是最后的倔犟~ Process finished with exit code 0
解釋
從第打印圖可以看出,攜程創(chuàng)建了新的線程
DefaultDispatcher-worker-1
來執(zhí)行,不在主線程,所以全局攜程體和全局攜程體外的代碼是在不同線程中異步執(zhí)行的。
全局攜程創(chuàng)建的是守護線程,而主線程不是,所以當進程中所有非守護線程執(zhí)行完,進程就會退出,守護進程也將不復存在。
這就是為什么上面例子不能執(zhí)行打印代碼的原因。守護線程
是指為其他線程服務的線程。在JVM中,所有非守護線程都執(zhí)行完畢后,無論有沒有守護線程,虛擬機都會自動退出。因此,JVM退出時,不必關心守護線程是否已結(jié)束kotlin攜程創(chuàng)建的線程對象是
CoroutineScheduler中Worker內(nèi)部類
,看一下這個內(nèi)部類的初始化。默認就是守護線程。如果大家想要驗證,可以使用jps
打印當前執(zhí)行的Java進程,在用jstack
查看進程中相關線程的情況。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的擴展函數(shù),所以必須在攜程體內(nèi)才可以調(diào)用。輸出如下:main======局部攜程~ Process finished with exit code 0
runBlocking
會阻塞當前線程并且等待,在所有已啟動的子協(xié)程執(zhí)行完畢之前不會結(jié)束。所以launch啟動就是runBlocking子攜程,因為launch在runBlocking攜程作用域中。在看一個例子:fun main(): Unit = runBlocking { GlobalScope.launch { delay(2000L) println("${treadName()}======全局攜程") } // 如果沒有下面的代碼,上面代碼不會執(zhí)行 launch { delay(1000L) println("${treadName()}======局部攜程") } }
輸出如下:
main======局部攜程 Process finished with exit code 0
解釋
GlobalScope.launch啟動是全局攜程,會重新新建一個線程來執(zhí)行,并不是runBlocking的子攜程。所以并不會等待GlobalScope.launch攜程體執(zhí)行完再退出進程。
-
suspend fun main() { // 聲明攜程作用域,掛起函數(shù),會釋放底層線程用于其他用途,創(chuàng)建一個協(xié)程作用域并且在所有已啟動子協(xié)程執(zhí)行完畢之前不會結(jié)束 coroutineScope { // 在該攜程作用域啟動攜程 launch { delay(3000L) println("${treadName()}======才開始學習coroutines") } } println("${treadName()}======最后的倔犟~") }
這種方式啟動的攜程作用域就在
coroutineScope
內(nèi)。注意日志線程名字
,輸出如下:DefaultDispatcher-worker-1======才開始學習coroutines DefaultDispatcher-worker-1======最后的倔犟~ Process finished with exit code 0
從日志發(fā)現(xiàn),
main
居然不上在主線程執(zhí)行的,其實并不是這樣,反編譯kotlin代碼,發(fā)現(xiàn)main
主入口代碼變成這樣了RunSuspendKt.runSuspend(new KotlinShareKt$$$main(var0))
。其實coroutineScope
就是創(chuàng)建一個攜程環(huán)境。在看一個復雜的點的例子
fun main() = runBlocking { launch { delay(2000L) println("${treadName()}======Task from runBlocking") } coroutineScope { // 創(chuàng)建一個協(xié)程作用域 launch { delay(1000L) println("${treadName()}======Task from nested launch") } delay(100L) println("${treadName()}======Task from coroutine scope") // 這一行會在內(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{...}
可以看著也是一個子攜程體,調(diào)用掛起函數(shù)delay。而launch {...}和coroutineScope{...}后面的代碼誰先執(zhí)行就要看launch中delay延遲的時間了。 -
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
-
需要引入庫androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02
使用:
lifecycleScope.launch {}
-
在實踐中絕大多數(shù)取消一個協(xié)程的理由是它有可能超時。 當你手動追蹤一個相關Job
的引用并啟動了一個單獨的協(xié)程在延遲后取消追蹤,這里已經(jīng)準備好使用withTimeout
函數(shù)來做這件事。withTimeout(1300L) { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } }
擴展
由于取消只是一個例外,所有的資源都使用常用的方法來關閉。 如果你需要做一些各類使用超時的特別的額外操作,可以使用類似withTimeout
的withTimeoutOrNull
函數(shù),并把這些會超時的代碼包裝在try {...} catch (e: TimeoutCancellationException) {...}
代碼塊中,而withTimeoutOrNull
通過返回null
來進行超時操作,從而替代拋出一個異常。 -
suspend fun doSomethingUsefulOne(): Int { delay(1000L) // 假設我們在這里做了一些有用的事 return 13 } suspend fun doSomethingUsefulTwo(): Int { delay(1000L) // 假設我們在這里也做了一些有用的事 return 29 }
- 默認順序調(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")
- 惰性啟動的 async
可選的,async
可以通過將start
參數(shù)設置為CoroutineStart.LAZY
而變?yōu)槎栊缘摹?在這個模式下,只有結(jié)果通過await
獲取的時候協(xié)程才會啟動,或者在Job
的 start`函數(shù)調(diào)用的時候。val time = measureTimeMillis { val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() } val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() } // 執(zhí)行一些計算 one.start() // 啟動第一個 two.start() // 啟動第二個 println("The answer is ${one.await() + two.await()}") } println("Completed in $time ms")
- 默認順序調(diào)用
-
依然例子先行:fun main() = runBlocking{ val job = GlobalScope.launch { // 啟動一個新協(xié)程并保持對這個作業(yè)的引用 delay(1000L) println("World!") } println("Hello,") job.join() // 等待直到協(xié)程執(zhí)行結(jié)束 }
輸出如下:
Hello, World! Process finished with exit code 0
按照之前的講解,
GlobalScope.launch
啟動的是全局攜程,并不屬于runBlocking的子攜程,所以runBlocking不會等待該攜程執(zhí)行完畢再退出進程,那為什么這里會等待呢,那這就是join函數(shù)的功勞,join作用是掛起協(xié)程直到攜程執(zhí)行完成。 -
協(xié)程的取消是 協(xié)作 的。一段協(xié)程代碼必須協(xié)作才能被取消。 所有kotlinx.coroutines
中的掛起函數(shù)都是 可被取消的 。它們檢查協(xié)程的取消, 并在取消時拋出CancellationException
然而,如果協(xié)程正在執(zhí)行計算任務,并且沒有檢查取消的話,那么它是不能被取消的。val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextPrintTime = startTime var i = 0 while (i < 5) { // 一個執(zhí)行計算的循環(huán),只是為了占用 CPU // 每秒打印消息兩次 if (System.currentTimeMillis() >= nextPrintTime) { println("job: I'm sleeping ${i++} ...") nextPrintTime += 500L } } } delay(1300L) // 等待一段時間 println("main: I'm tired of waiting!") job.cancelAndJoin() // 取消一個作業(yè)并且等待它結(jié)束 println("main: Now I can quit.")
打印輸出并沒在控制臺上看到堆棧跟蹤信息的打印。這是因為在被取消的協(xié)程中
CancellationException
被認為是協(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) // 延遲一段時間 println("main: I'm tired of waiting!") job.cancelAndJoin() // 取消該作業(yè)并且等待它結(jié)束 println("main: Now I can quit.") }
-
運行不能取消的代碼塊
在前一個例子中任何嘗試在finally
塊中調(diào)用掛起函數(shù)的行為都會拋出CancellationException
,因為這里持續(xù)運行的代碼是可以被取消的。通常,這并不是一個問題,所有良好的關閉操作(關閉一個文件、取消一個作業(yè)、或是關閉任何一種通信通道)通常都是非阻塞的,并且不會調(diào)用任何掛起函數(shù)。然而,在真實的案例中,當你需要掛起一個被取消的協(xié)程,你可以將相應的代碼包裝在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) // 延遲一段時間 println("main: I'm tired of waiting!") job.cancelAndJoin() // 取消該作業(yè)并等待它結(jié)束 println("main: Now I can quit.")
結(jié)束語
很多例子都是官網(wǎng)的,只是加上一些自己的理解,這篇文章只是帶大家快速入門kotlin攜程使用,后面會逐步深入,講解攜程的實現(xiàn)原理。