iT邦幫忙

2021 iThome 鐵人賽

DAY 5
1
Mobile Development

解鎖kotlin coroutine的各種姿勢-新手篇系列 第 5

day 5 knock, knock我要開始coroutine

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,回來更新


上一篇
day 4 I'm your father, coroutine父子繼承關係
下一篇
day6 阿伯出事啦 exception
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
qsc236578
iT邦新手 5 級 ‧ 2023-01-10 10:31:21

您好 我按照你的步驟但有些疑問想請教您
當我註解了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

Kenny iT邦新手 3 級 ‧ 2023-01-20 23:11:45 檢舉

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

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

0
MumiRabbit
iT邦新手 4 級 ‧ 2024-02-01 14:52:32

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

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

我要留言

立即登入留言