【kotlin】- 攜程基本使用

簡介

隨著kotlin不斷普及,以其簡潔的語法糖,易擴展,空安全,汲取了不同語言的優(yōu)點等...越來越受到開發(fā)者的青睞。剛?cè)?code>kotlin,除了和Java不一樣的語法讓人難以習慣外,“攜程”和“泛型”更是讓開發(fā)者頭疼。接下來由我?guī)Т蠹伊私鈑otlin攜程基本使用。

其它文章

【kotlin】- delay函數(shù)實現(xiàn)原理
【kotlin】- 攜程的執(zhí)行流程
【kotlin】- 攜程的掛起和恢復

創(chuàng)建攜程

  • \color{blue}{kotlin中使用Thread}
    如果在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日志是打印不出來的喲,原因可以百度一下守護線程

  • \color{blue}{啟動一個全局攜程}

    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}
    }
    
  • \color{blue}{啟動子攜程}

    // 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í)行完再退出進程。

  • \color{blue}{coroutineScope聲明攜程作用域}

    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延遲的時間了。

  • \color{blue}{CoroutineScope構(gòu)建攜程}

    fun main() {
        val cs = CoroutineScope(Dispatchers.Default)
        cs.launch {  }
    }
    
  • \color{blue}{withContext在指定攜程上下文啟動攜程}
    使用給定的協(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
    
  • \color{blue}{Android在生命周期內(nèi)啟動攜程}
    需要引入庫

    androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02
    

    使用:

    lifecycleScope.launch {}
    
  • \color{blue}{攜程超時}
    在實踐中絕大多數(shù)取消一個協(xié)程的理由是它有可能超時。 當你手動追蹤一個相關 Job的引用并啟動了一個單獨的協(xié)程在延遲后取消追蹤,這里已經(jīng)準備好使用 withTimeout 函數(shù)來做這件事。

    withTimeout(1300L) {
       repeat(1000) { i ->
           println("I'm sleeping $i ...")
           delay(500L)
       }
    }
    

    擴展
    由于取消只是一個例外,所有的資源都使用常用的方法來關閉。 如果你需要做一些各類使用超時的特別的額外操作,可以使用類似 withTimeoutwithTimeoutOrNull 函數(shù),并把這些會超時的代碼包裝在 try {...} catch (e: TimeoutCancellationException) {...} 代碼塊中,而 withTimeoutOrNull通過返回 null 來進行超時操作,從而替代拋出一個異常。

  • \color{blue}{組合掛起函數(shù)}

    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")
      
  • \color{blue}{攜程join}
    依然例子先行:

    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í)行完成。

  • \color{blue}{攜程取消}
    協(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)原理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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