Kotlin協程教程(3):操控協程

在以前的文章中,已經講了如何啓動協程、協程的做用域是如何組織和工做的以及各類協程構造器(builder)的特性。網絡

本篇將講解對協程的各類操做,包括掛起、取消、超時、切換上下文等。async

掛起

fun main()  {
    runBlocking(Dispatchers.Default) {
        for (i in 0 .. 10) {
            println("aaaaa ${Thread.currentThread().name}")
            delay(1000) // 這是一個掛起函數
            println("bbbbb ${Thread.currentThread().name}")
        }
    }
}

delay就是一個掛起函數,掛起的意思是:非阻塞的暫停,與之對應的就是阻塞(的暫停)。好比線程的方法Thread.sleep就是一個阻塞的方法。關於阻塞仍是非阻塞,能夠簡單的理解爲:函數

  • 阻塞就是cpu不執行後面的代碼,須要某種通知告訴線程繼續執行。
  • 非阻塞就是cpu依然在執行線程的代碼,非阻塞的暫停只是經過用戶態的程序邏輯讓代碼塊不執行而已。

用圖來表示線程阻塞的狀況應該是這樣:ui

clipboard.png

而在協程中,非阻塞的狀況應該是這樣:spa

clipboard.png

能夠看到,線程的阻塞,那這個線程就真的不去作事情了,必須等到被喚醒了,纔會繼續執行,在被喚醒以前,這個線程資源能夠說就被浪費了,若是我有新的任務,就必須在啓動一個新的線程來執行。線程

可是協程上的掛起,它會去尋找有沒有須要執行的代碼塊,若是有,就拿來跑,這樣就能更高效的利用線程資源。若是掛起後,也沒有發現任何能夠執行的代碼塊,一樣的也會進入阻塞狀態,這一點和線程是同樣的。code

在kotlin中,掛起函數只能在協程環境中使用。協程

等待與取消

等待一個協程執行完畢,和線程的API一致,使用join方法就能夠了。blog

val job = launch {
    // ....
}

job.join()

若是須要返回值,也可使用async來啓動協程,使用await方法來等待完成,並取得返回值數據。ip

val job = async {
    // ....
}

job.await()

await和join都是掛起函數。

協程應該被實現爲能夠被取消的,調用Job的cancel方法能夠取消。可是,若是咱們寫個while(true)的死循環怎麼取消呢?

顯然是取消不了的。

爲了能讓咱們的協程邏輯能被取消,就須要使用到協程的一個屬性isActive。

假設咱們有一個協程是下載一個文件,咱們想讓它能被取消。它多是這樣:

val dlJob = launch {
    var isFinished = false
    while (!isFinished) {
        // download ...
        
        if (dlSize == totalSize) {
            isFinished = true
        }
    }
}

這樣的話,這個協程是沒法被取消的,它沒法被外側所操控,咱們可使用isActive來改寫一下。

val dlJob = launch {
    var isFinished = false
    while (!isFinished && isActive) { // 注意這裏
        // download ...

        if (dlSize == totalSize) {
            isFinished = true
        }
    }
}

只須要這樣,就能夠實現取消邏輯了。

問題也就隨之而來,像打開網絡鏈接,讀寫文件,老是須要去執行一些close的邏輯纔是符合規範的,若是協程被取消,就直接退出了,要如何才能回收打開的資源呢?

如何回收資源

能夠經過try{...}finally{...}進行回收資源,就像這樣:

val dlJob = launch {
    try {
        var isFinished = false
        while (!isFinished && isActive) { // 注意這裏
            // download ...

            if (dlSize == totalSize) {
                isFinished = true
            }
        }    
    } finally {
        // close something
    }
}

當job被取消後,finally方法裏面依然會在最後被執行,能夠在這裏進行一些回收的操做。

超時

若是咱們指望一個協程最多隻能執行多少時間,超過這個時間就要被取消的時候,就可使用超時邏輯,可使用withTimeout函數來實現。

runBlocking(Dispatchers.Default) {

    try {
        // 只容許協程執行最多500毫秒
        val job = withTimeout(500) { 
            try {
                println("working 1")
                delay(1000)
                println("working 2")
            } finally {
                println("finally, I will do something")
            }
        }

        println("job $job") // 沒法被執行到
    } catch (e: Throwable) {
        println("out coroutine $e")
    }

}

若是超時了,則會拋異常,而且,這個函數與runBlocking是同樣的,都會阻塞當前線程。上面的代碼中,協程外的print不會被執行到。

若是不想拋異常,可使用另外一個超時函數withTimeoutOrNull。

runBlocking(Dispatchers.Default) {

    try {
        // 只容許協程執行最多500毫秒
        val job = withTimeoutOrNull(500) {
            try {
                println("working 1")
                delay(1000)
                println("working 2")
            } finally {
                println("finally, I will do something")
            }
        }
    
        println("job $job") // 能夠被執行到
    } catch (e: Throwable) {
        println("out coroutine $e")
    }

}

最終運行的結果是:

working 1
finally, I will do something
job null

切換上下文

若是咱們指望協程的代碼在不一樣的線程中來回跳轉,可使用withContext來實現。(emmmmm,這是什麼場景的需求呢?)

newSingleThreadContext("Ctx1").use { ctx1 ->
    newSingleThreadContext("Ctx2").use { ctx2 ->
        runBlocking(ctx1) {
            log("Started in ctx1")
            withContext(ctx2) {
                log("Working in ctx2")
            }
            log("Back to ctx1")
        }
    }
}

這裏直接照搬文檔中的示例代碼,最後輸出的結果爲:

[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1

總結

以上就是操控協程的各類方法了。

掛起函數是協程中定義的概念,只能在協程中使用,掛起的含義是非阻塞的暫停,調度器會尋找須要運行的協程放到線程中去執行,若是找不到任何須要執行的協程,纔會將線程阻塞。

協程是能夠被取消的,任何系統提供的掛起函數內部都有取消的邏輯,若是本身的協程想要能夠被取消,就必須經過isActive變量來編寫邏輯。

取消後的協程老是會執行finally代碼塊,能夠在這裏進行一些資源回收的操做。

若是但願控制協程的工做時長,可使用withTimeout來限制協程。

經過withContext函數來將邏輯切換到其餘的線程上去。

以前的表格,就能夠獲得進一步的擴展了

clipboard.png

相關閱讀

若是你喜歡這篇文章,歡迎點贊評論打賞
更多幹貨內容,歡迎關注個人公衆號:好奇碼農君

相關文章
相關標籤/搜索