Kotlin協程基礎


開發環境

  • IntelliJ IDEA 2021.2.2 (Community Edition)
  • Kotlin: 212-1.5.10-release-IJ5284.40

我們已經通過第一個例子學會了啟動協程,這里介紹一些協程的基礎知識。

阻塞與非阻塞

runBlocking

delay是非阻塞的,Thread.sleep是阻塞的。顯式使用 runBlocking 協程構建器來阻塞。

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在后台啟動一個新的協程並繼續
        delay(200)
        "rustfisher.com".forEach {
            print(it)
            delay(280)
        }
    }
    println("主線程中的代碼會立即執行")
    runBlocking {     // 這個表達式阻塞了主線程
        delay(3000L)  //阻塞主線程防止過快退出
    }
    println("\n示例結束")
}

可以看到,runBlocking里使用了delay來延遲。用了runBlocking的主線程會一直阻塞直到runBlocking內部的協程執行完畢。
也就是runBlocking{ delay }實現了阻塞的效果。

我們也可以用runBlocking來包裝主函數。

import kotlinx.coroutines.*

fun main() = runBlocking {
    delay(100) // 在這里可以用delay了

    GlobalScope.launch {
        delay(100)
        println("Fisher")
    }
    print("Rust ")
    delay(3000)
}

runBlocking<Unit>中的<Unit>目前可以省略。

runBlocking也可用在測試中

// 引入junit
dependencies {
    implementation("junit:junit:4.13.1")
}

單元測試

使用@Test設置測試

import org.junit.Test
import kotlinx.coroutines.*

class C3Test {

    @Test
    fun test1() = runBlocking {
        println("[rustfisher] junit測試開始 ${System.currentTimeMillis()}")
        delay(1234)
        println("[rustfisher] junit測試結束 ${System.currentTimeMillis()}")
    }
}

運行結果

[rustfisher] junit測試開始 1632401800686
[rustfisher] junit測試結束 1632401801928

IDEA可能會提示no tasks available。需要把測試選項改為IDEA,如下圖。

test-setting.png

等待

有時候需要等待協程執行完畢。可以用join()方法。這個方法會暫停當前的協程,直到執行完畢。需要用main() = runBlocking

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("[rustfisher]測試等待")
    val job1 = GlobalScope.launch {
        println("job1 start")
        delay(300)
        println("job1 done")
    }
    val job2 = GlobalScope.launch {
        println("job2 start")
        delay(800)
        println("job2 done")
    }

    job2.join()
    job1.join() // 等待
    println("測試結束")
}

運行log

[rustfisher]測試等待
job1 start
job2 start
job1 done
job2 done
測試結束

結構化的並發

GlobalScope.launch時,會創建一個頂層協程。之前的例子我們也知道,它不使用主線程。新創的協程雖然輕量,但仍會消耗一些內存資源。如果忘記保持對新啟動的協程的引用,它還會繼續運行。

我們可以在代碼中使用結構化並發。

示例中,我們使用runBlocking協程構建器將main函數轉換為協程。在里面(作用域)啟動的協程不需顯式使用join

觀察下面的例子:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    println("主線程id ${Thread.currentThread().id}")
    launch { // 在 runBlocking 作用域中啟動一個新協程1
        println("協程1所在線程id ${Thread.currentThread().id}")
        delay(300)
        println("協程1執行完畢")
    }
    launch { // 在 runBlocking 作用域中啟動一個新協程2
        println("協程2所在線程id ${Thread.currentThread().id}")
        delay(500)
        println("協程2執行完畢")
    }
    println("主線程執行完畢")
}

運行log

主線程id 1
主線程執行完畢
協程1所在線程id 1
協程2所在線程id 1
協程1執行完畢
協程2執行完畢

可以看到,不用像之前那樣調用Thread.sleep或者delay讓主線程等待一段時間,防止虛擬機退出。

程序會等待它所有的協程執行完畢,然后真正退出。

作用域構建器

使用 coroutineScope 構建器聲明自己的作用域。它會創建一個協程作用域,並且會等待所有已啟動子協程執行完畢。

runBlockingcoroutineScope 看起來類似,因為它們都會等待其協程體以及所有子協程結束。主要區別在於:

  • runBlocking 方法會阻塞當前線程來等待,是常規函數
  • coroutineScope 只是掛起,會釋放底層線程用於其他用途,是掛起函數

下面這個示例展示了作用域構建器的特點。main是一個作用域。

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch {
        delay(200L)
        println("協程1 t${Thread.currentThread().id}")
    }

    coroutineScope { // 創建一個協程作用域
        launch {
            delay(500L)
            println("內部協程2-1 t${Thread.currentThread().id}")
        }

        delay(100L)
        println("協程2 t${Thread.currentThread().id}")
    }

    println("主任務完畢")
}

運行log

協程2 t1
協程1 t1
內部協程2-1t1
主任務完畢

提取函數重構

launch { …… } 內部的代碼塊提取到獨立的函數中。提取出來的函數需要 suspend 修飾符,它是掛起函數

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking<Unit> {
    launch { r1() }
    println("DONE")
}

// 掛起函數
suspend fun r1() {
    delay(300)
    println("[rustfisher] 提取出來的函數")
}

log

DONE
[rustfisher] 提取出來的函數

協程是輕量的

我們前面也試過,創建非常多的協程,程序運行OK。

下面的代碼可以輸出很多的點

import kotlinx.coroutines.*

fun main() = runBlocking {
    for (t in 1..10000) {
        launch {
            delay(t * 500L)
            print(".")
        }
    }
}

全局協程像守護線程

我們在線程介紹中知道,如果進程中只剩下了守護線程,那么虛擬機會退出。
前文那個打印rustfisher.com的例子,其實也能看到,字符沒打印完程序就結束了。

GlobalScope 中啟動的活動協程並不會使進程保活。它們就像守護線程。

再舉一個例子

import kotlinx.coroutines.*

fun main() = runBlocking {
    GlobalScope.launch {
        for (i in 1..1000000) {
            delay(200)
            println("協程執行: $i")
        }
    }

    delay(1000)
    println("Bye~")
}

log

協程執行: 1
協程執行: 2
協程執行: 3
協程執行: 4
Bye~

最后我們來看一下全文的思路

guide.png

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM