iT邦幫忙

2021 iThome 鐵人賽

DAY 7
0
Mobile Development

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

day7 我不要了,這不是肯德基 cancel

Cancellation is important for avoiding doing more work than needed which can waste memory and battery life;

當我們在開發時,很難去追蹤每一個coroutine或是一個一個去取消,所以才會透過scope的取消去cancel他所有的子coroutine,而cancel只會影響其child coroutine對他的sibling或parent不會有影響

coroutine的cancel會丟出CancellationException

fun cancel(cause: CancellationException? = null)

實際運行上是由child通知一個Exception, parent檢查他是甚麼Exception,如果是CancellationException,parent就不會執行動作,反之則會向上傳遞(參閱Exception)

當一個coroutine被cancel之後,便無法再往裡面加入新的coroutine,然而當我們呼叫cancel()的時候,coroutine並不會立刻關閉,而是會先轉移到canceling state等到工作結束後再轉換成cancelled state


val scope = CoroutineScope(Job())
val rootJob = scope.launch {
    for (i in (1..10)){
        Thread.sleep(100)
        Timber.d(i.toString())
    }
}
lifecycleScope.launch {
    delay(300)
    Timber.d("isActive ${rootJob.isActive}, isCancelled ${rootJob.isCancelled}")
    rootJob.cancel()
    Timber.d("ask cancel")
    Timber.d("isActive ${rootJob.isActive}, isCancelled ${rootJob.isCancelled}")
}
/**
 * 1
 * 2
 * 3
 * isActive true, isCancelled false
 * ask cancel
 * isActive false, isCancelled true
 * 4
 * 5
 * 6
 * 7
 * 8
 * 9
 * 10
 * */

解釋一下,為甚麼要用Thread.sleep而不用delay

簡單講的話,delay會檢查Job的狀態,如果isCancelled == true,就會發出CancellationException

取消是協作的

我們希望我們的coroutine是可被取消的,所以需要定期檢查或是在執行長時間任務前檢查,透過job.isActive or ensureActive()或是透過yield關鍵字

檢查Job狀態

儘管delay()會自動檢查job的狀態,但我們沒事不會用他呀,還是要了解一下怎麼用job的檢查,他其實也很直白,isActive和ensureActive(),你們看了就會懂得

for (i in (1..10)){
    if (this.isActive){

        Thread.sleep(100)
        Timber.d(i.toString())
    }
}

//or
for (i in (1..10)){
    ensureActive()
    Thread.sleep(100)
    Timber.d(i.toString())
}
//or
while(i <= 10 && this.isActive)

yield

這裡就yield關鍵字做介紹,我覺得網路上儘管資源很多,但我通常看完還是一臉矇
meme

python yield
python yield
kotlin yield

這邊給三篇我覺得講得夠簡單,又有切到概念的文章

在文中有講到,yield在其他語言大概是 “return the value,and continue when you next enter。”這個意思,當然學程式總是離不開文檔,這邊就以文檔的例子講解

// inferred type is Sequence<Int>
val fibonacci = sequence {
    yield(1) // first Fibonacci number
    var cur = 1
    var next = 1
    while (true) {
        yield(next) // next Fibonacci number
        val tmp = cur + next
        cur = next
        next = tmp
    }
}

println(fibonacci.take(10).joinToString())
//1, 1, 2, 3, 5, 8, 13, 21, 34, 55

有懂嗎?會讓出當前thread,讓其他任務執行,如果讓出後沒有其他任務要在該thread執行,會繼續執行
Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutines on the same dispatcher to run if possible.
注意,只有在同個thread的任務可以透過yield切換,如果用main和io去跑,yield並不會有作用

lifecycleScope.launch {
    Timber.d("main1")
    yield()

    Timber.d("main2")
    yield()

    Timber.d("main3")

}

lifecycleScope.launch {
    Timber.d("main -2 1")
    yield()

    Timber.d("main -2 2")
    yield()

    Timber.d("main -2 3")
}
/**
 * main1
 * main -2 1
 * main2
 * main -2 2
 * main3
 * main -2 3
 * */

可是等等,這個特性並不能保證job的狀態檢查呀
沒錯,他和delay類似,會自動檢查job狀態,但我認為在需要上述特性時,再用這個關鍵字會更好,不然用isActive或ensureActive()即可,也能避免粗心而發生執行順序亂掉

在取消時執行任務

當job執行到canceling,狀態時,儘管還沒結束,他卻不能再執行suspend,但如果用try/catch/finally的模式,或是有某種特殊需求,必須這麼做(別問我,我想不到

runBlocking<Unit> {
   val job = launch (Dispatchers.Default) {
        try {
        	delay(1000L)
        } finally {
            withContext(NonCancellable){
                delay(1000L)
                println("Cleanup done!")
        	}
        }
    }
    delay(1000L)
    println("Cancel!")
    job.cancel()
    println("Done!")
}

注意,沒事不要用,要用時只能用withContext
如果用launch(NonCancellable),parent取消時,child會留著,鑽石恆久遠,一顆永流傳的概念,直到整個application銷毀,並且parent不會也不會再child throw Exception時被銷毀,簡單說,問題很多,到時抓bug抓到哭出來

Doing this is very risky as you lose control of the execution of the coroutine. It’s true that it produces more concise and easier to read code but the problems this can cause in the future are unpredictable.

  1. 無法在測試時取消
  2. 在無限循環中,delay或任何檢查job的方式都將無法取消
  3. 以此建構flow,flow將無法從外部取消

Recommendation: use it ONLY for suspending cleanup code.

連結整理

了解yield

python yield
python yield
kotlin yield

文檔

官方blog cancel
英文文檔看exception和cancel
中文文檔看Exception和cancel

官方blog pattern for work shouldn't be cancel


上一篇
day6 阿伯出事啦 exception
下一篇
day8 kotlin coroutine的 runBlocking, withContext
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30

尚未有邦友留言

立即登入留言