2021 iThome 鐵人賽
Mobile Development
DAY 5

day 5 knock, knock我要開始coroutine

解鎖kotlin coroutine的各種姿勢-新手篇 系列 第 5 篇
Kenny
4 年前 ‧ 4131 瀏覽

coroutine神奇又好用,那我要怎麼開始呢?
官方提供了兩種方法,launch和 async

launch

launch的意思,大概是我要這裡創造新的coroutine,並在指定的Thread之中運行,這裡是哪裡呢?
只要是coroutinescope裡面,或是suspend function都可以(因為suspend一定要在coroutineScope或suspend function裡面執行)

而launch的特性是他不會在coroutine結束時返回值,多數情況都會以launch創建coroutine
start a new coroutine that is “fire and forget” — that means it won’t return the result to the caller.

val scope = CoroutineScope(rootJob)
        
scope.launch ( Dispatchers.IO ){
//code
}
//看原始碼,我們寫在coroutine裡的代碼塊會被標記上suspend
fun CoroutineScope.launch {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
}

async

那async要怎麼用?

當兩個任務之間沒有相互關係時,async併發可以讓我們減少等待執行的時間,並回傳deferred類型,因為無法確定非同步任務會在何時結束,不論是運算結果或是Exception,他都預期開發者會呼叫await()掛起函數,以獲得回傳值或是錯誤訊息,所以預設並不會丟出exception

val scope = CoroutineScope(rootJob)
scope.async {

}.await()

可以比較一下原始碼

fun CoroutineScope.async() {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
}: Deferred<T>

只看文字應該會覺得似懂非懂,這裡我們再帶個小範例


val task: (String) ->Unit = { Timber.d("longTimeTask in $it") }
fun longTimeTask(): (String) -> Unit {
    return task
}

val scope = CoroutineScope(rootJob)

val notRelateWork1 = scope.async {
    Timber.d("in async 1")
    longTimeTask()
}
val notRelateWork2 = scope.async {
    longTimeTask().invoke("async 2")
    Timber.d("in async 2")
}
scope.launch {
    Timber.d("in launch")
    notRelateWork1.await().invoke("async 1") //(String) -> Unit
    notRelateWork2.await() //Unit
}


//in async 1
//longTimeTask in async 2
//in async 2
//in launch
//longTimeTask in async 1

這裡的順序非常重要,儘管我們需要呼叫await,來拿到async回傳參數,但在async回傳值之前的代碼會先被執行,而在async裡面默認是回傳最後一行的值,所以我搭配lambda,大家應該很容易就能看出呼叫執行task的地方差異了

重點:launch和async很大的差別在於

  1. 他們如何處理exception,async預期開發者會呼叫await來獲得結果(或是exception),換句話說,他會默默地丟出exception,而只有在呼叫await你才會知道拿到result或是exeption,而launch會直接丟出來
  2. async會回傳deferred< value>,而launch不回傳任何東西,是fire and forget的coroutine

Warning: A big difference between launch and async is how they handle exceptions. async expects that you will eventually call await to get a result (or exception) so it won’t throw exceptions by default. That means if you use async to start a new coroutine it will silently drop exceptions.

async併發

我們介紹完了await()和async的基本知識,但剛剛提過 當兩個任務之間沒有相互關係時,async併發可以讓我們減少等待執行的時間, 具體來說,還是看code比較實際

今天如果用launch要等兩秒,但async併發只要等1秒多一點,今天如果是有10個任務,launch要等10秒,但async一樣是一秒多

suspend delay1000():Int{
    delay(1000)
    //pretend a api call
    return 1
}
val time = measureTimeMillis {
    val one = async { delay1000() }
    val two = async { delay1000() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

//或分開寫
val time = measureTimeMillis {
    println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { delay1000() }
    val two = async { delay1000() }
    one.await() + two.await()
}

我實在找不到圖說明這個步驟,用文字解釋就是在delay1000(),裡面的delay是suspend function,在launch裡面時,suspend會依順序觸發,而在async代碼執行到那裡可被掛起,讓thread可以先執行其他任務,當delay()結束後,會切回delay後面,繼續執行其他代碼

延遲執行

剛才講到了代碼的執行順序,但如果我們希望在呼叫的時候才執行,有沒有像 kotlin lazy的方法呢?有,也是lazy

我覺得很清楚了,應該不用解釋吧

launch lazy

launch(start = CoroutineStart.LAZY) { delay1000() }

async lazy

惰性的併發
只有通过 await 或者在 Job 的 start 函数调用的时候协程才会启动

val time = measureTimeMillis {
    val one = async(start = CoroutineStart.LAZY) { delay1000() }
    val two = async(start = CoroutineStart.LAZY) { delay1000() }
    // 执行一些计算
    one.start() // 启动第一个
    two.start() // 启动第二个
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

要注意,如果不用start只用await,async coroutine會依序啟動,而不會變成併發啟動

launch/async 後發生什麼事

當我們開啟一個coroutine後,他就是 thread嗎?

不對不對,我們之前講過,kotlin 的coroutine只是一個線程框架,他是將線程包裝成方便的版本,launch不是 thread,Dispatcher也不是thread,而這段Code實際上會是這樣

val scope = CoroutineScope(rootJob)
        
scope.launch ( Dispatchers.IO ){
//code
}

//變成
handler.post{
   //code from launch block
   //launch裡面的代碼
}

launch和Dispatcher都不是thread本身,他們是將任務透過handler post到 thread 的looper裡面。

參考連結:
必看
中文文檔
android文檔
英文async文檔
github async 文檔

中秋節來個應景梗圖,中秋節快樂
meme

放假太爽,day 5打成day 4,回來更新

此系列
上一篇
此系列
下一篇

2 則留言

qsc236578
.2 年前

您好 我按照你的步驟但有些疑問想請教您
當我註解了work2 但實際上 work2內容仍然被執行了,請問這是什麼原因呢 ?

scope.launch {
Log.d("longTimeTask", "Start Launch")
work1.await().invoke("I am task One")
// work2.await()
}

private val work1 = scope.async {
Log.d("longTimeTask", "async work1 ")
longTimeTask()
}
private val work2 = scope.async {
longTimeTask().invoke("async 2 invoke")
Log.d("longTimeTask", "async work2 ")
}

※console
D/longTimeTask: async work1
D/longTimeTask: From task async 2 invoke
D/longTimeTask: async work2
D/longTimeTask: Start Launch
D/longTimeTask: From task I am task One

1 則回應 分享
Kenny 2 年前

哈囉,因為在生成 work2 變數的時候,就已經在 scope 之下,用 async 產出了一個 childCoroutineScope,而 async 的特性是並行,並在 await 回傳

換句話說, async 裡面的程式碼會在執行緒有空閒的時候執行,如果任務先結束了,會留一個 Deffered 類型的資料,等到有人呼叫 await 的時候,可以立刻回傳值,也就是說 async 裡面的程式碼,即使沒有被 await 也會執行,套件設計上預期開發者會記得呼叫 await,而 ide 好像也會提醒

登入發表回應
MumiRabbit
.1 年前

不好意思我不太懂您這段意思
今天如果用launch要等兩秒,但async併發只要等1秒多一點,今天如果是有10個任務,launch要等10秒,但async一樣是一秒多

在我看來launch 開十個也還是一秒多
有差距的只是async 是defer , launch是job吧?

0 則回應 分享
登入發表回應