協程

Coroutine是一個輕量級的非同步API,在很多語言裡面都會使用“async”與“await”關鍵字來實現非同步,可是在Kotlin裡面並沒有這兩個關鍵字。相對的Coroutine使用“suspend fun”來使程式暫停,調用其他執行緒來協同運作,接下來我們就來看看要怎麼使用Coroutine吧!

第一個協程程式

fun main() = runBlocking {
    launch {
        delay(1000)
        println("小黑!")
    }
    println("Hello")
}

runBlocking

runBlocking可以開啟一個新的Coroutine,並且阻塞現在的執行緒,等到裡面的東西全部執行完畢才會繼續。因為這個會阻塞現有執行緒的特性,所以在Android專案中“絕對不可以拿來使用”,在這裡拿來開啟Coroutine並且讓程式可以等到Coroutine執行完畢再結束使用

launch

launch一樣可以拿來開啟一個新的協程,並且會與現有的協程同時進行

suspend fun

suspend fun可以暫停當前的工作,並且釋放這個執行緒給其他工作使用,等到suspend fun完成之後再繼續執行接下來的工作,suspend fun必須在Coroutine裡面呼叫,或是在其他suspend fun裡面呼叫

fun main() = runBlocking {
    launch {
        showName()
    }
    println("Hello")
}

suspend fun showName() {
    delay(1000)
    println("小黑!")
}

coroutineScope

還有一個方式可以建立一個新的Coroutine就是使用coroutineScope函數,他是一個suspend fun並且也是會等裡面的Coroutine全部完成後才繼續執行。跟runBlocking不同的是runBlocking會把現有的執行緒塞住,不過coroutineScope只是把工作暫停,把資源讓給其他工作使用

fun main() = runBlocking {
    doWorld()
    println("Done")
}

suspend fun doWorld() = coroutineScope {
    launch {
        delay(2000L)
        println("World 2")
    }
    launch {
        delay(1000L)
        println("World 1")
    }
    println("Hello")
}

Job

launch函數會回傳一個Job,一般來說launch不會暫停現有的工作,不過我們可以使用Job裡面的join函數來暫停工作

fun main() = runBlocking {
    doWorld()
    println("Done")
}

suspend fun doWorld() = coroutineScope {
    val job = launch {
        delay(2000L)
        println("World 2")
    }
    launch {
        delay(1000L)
        println("World 1")
    }
    job.join()
    println("Hello")
}

取消Job

所有的suspend fun都是可以被取消的,當我們取消Coroutine的時候suspend fun會拋出Cancellation Exception,當Coroutine接到這個例外的時候就會取消

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancel()
    job.join()
    println("main: Now I can quit.")
}

不過如果我們在Coroutine裡面沒有suspend fun或是我們把Cancellation Exception捕捉起來沒有拋出的話Coroutine就不會被取消

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) {
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
}
fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        repeat(5) { i ->
            try {
                println("job: I'm sleeping $i ...")
                delay(500)
            } catch (e: Exception) {
                println(e)
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
}

如果在while迴圈裡面沒有使用suspend fun的話,為了可以成功取消Coroutine,我們必須使用isActive來判斷

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) {
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
}

不可被取消的區塊

有些時候我們有一些工作會希望能夠完全跑完再結束,這個時候我們就可以使用“withContent(NonCancellable) {...}”來達成

fun main() = runBlocking {
    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()
    println("main: Now I can quit.")
}

withTimeout

withTimeout會在Coroutine執行超過設定時間後拋出錯誤

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

我們可以使用try/catch捕捉錯誤處理,或是使用withTimeoutOrNull

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(10000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // will get cancelled before it produces this result
    }
    println("Result is $result")
}

同時執行suspend fun

在使用Coroutine的時候,我們會編寫很多suspend fun,不過suspend fun一般來說並沒有辦法同時執行,我們可以看看下面的例子

suspend fun doSomethingOne(): Int {
    delay(1000L)
    return 13
}

suspend fun doSomethingTwo(): Int {
    delay(1000L)
    return 29
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val one = doSomethingOne()
        val two = doSomethingTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
}

這個時候我們可以使用Coroutine裡面提供的async、await函數來讓兩個函數可以同時執行

val time = measureTimeMillis {
    val one = async { doSomethingOne() }
    val two = async { doSomethingTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

控制Coroutine開始執行的時間

Coroutine預設在創建的時候就會開始執行,如果我們希望創建的時候先不開始執行,讓我們自行決定什麼時候開始執行的話,可以把start設定成CoroutineStart.LAZY。這樣我們在想開始的時候調用start函數就可以了

fun main() = runBlocking {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingTwo() }
        one.start()
        two.start()
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

Dispatchers

launch、async、withContext這三個函數有一個“CoroutineContext”的參數,我們會在這裡傳入Dispatchers來決定接下來的Coroutine要在哪個執行緒上執行。常用的Dispatcher有:

  • Main:主執行緒,拿來處理UI相關的工作

  • IO:適合用來做資料讀取、網路請求等工作

  • Default:適合做資料處理、計算的工作

CoroutineContext

coroutineContext紀錄著這個coroutine運行時候的環境,包含:Job、執行緒等等,當我們開啟一個新的Coroutine的時候會使用原本的環境加上我們新的設定變成一個新的環境。而其中Job就是控制取消的關鍵,如果我們在建立Coroutine的時候使用了新的Job的話,這個新的Coroutine就不會被一起取消

Last updated