協(xié)程生命周期的最后一步---協(xié)程取消

任何對象都有生命周期,協(xié)程也不例外,其生命周期很簡單啟動(dòng)->運(yùn)行->結(jié)束。而每個(gè)生命周期的狀態(tài)轉(zhuǎn)換都是需要觸發(fā)條件的,比如啟動(dòng)->運(yùn)行,需要協(xié)程構(gòu)建器launch{},運(yùn)行期間需要續(xù)體,線程池等,最后結(jié)束的直接觸發(fā)條件就是本文要探討的內(nèi)容

線程的取消

因?yàn)閰f(xié)程的取消和線程的取消在原理上是非常相近的,而線程又是讀者比較容易接受的知識,所以在探討協(xié)程的取消前,先來看線程是如何取消的。

如何取消線程
  • stopsuspend:在底層上存在嚴(yán)重的缺陷,應(yīng)該避免使用此類方式停止線程
  • 中斷interrupted方法:這是一種協(xié)作機(jī)制,簡言之就是線程A設(shè)置一個(gè)線程B的中斷的flag,B在耗時(shí)循環(huán)體內(nèi)檢查該flag,true則放棄該循環(huán),則線程B會(huì)自行停止。提供如下實(shí)例代碼,簡單說明其協(xié)作原理:
public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        try {
            for (int i = 0; i < 500000; i++) {
                // check中斷標(biāo)志位
                if (this.isInterrupted()) {
                    System.out.println("stop");
                    // 異常法,使線程自行停止
                    throw new InterruptedException();
                }
            }
        } catch (InterruptedException e) {
            System.out.println("run catch");
            // to do someThing
        }
    }

    public static void main(String[] args) {
        try {
            Thread.sleep(2000);
            MyThread thread = new MyThread();
            thread.start();
            // 停止線程,設(shè)置中斷標(biāo)志位
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

附:這樣我們就知道官方協(xié)程指南中提到"取消是協(xié)作的”是什么意思了,其實(shí)原理和這個(gè)類似。

協(xié)程的取消

和線程的取消一樣,協(xié)程的取消也是協(xié)作機(jī)制,類似線程的interrupt()函數(shù),協(xié)程可以調(diào)用cancel()函數(shù)取消,或者cancelAndJoin()。類似isInterrupted(), 協(xié)程可以check isActive的值去判斷協(xié)程是否取消。

cancelcancelAndJoin()
  • cancel直接取消協(xié)程,即在耗時(shí)循環(huán)體內(nèi)拋CancellationException異常,不會(huì)等待耗時(shí)循環(huán)體后面的代碼執(zhí)行完成, 如下例子可表明
 suspend fun test2() {
    coroutineScope {
        val job = launch(Dispatchers.Default) {
            log("1")
            val file = File("E:\\Opera_64.0.3417.54_Setup_x64.exe")
            val bufferedReader = BufferedReader(FileReader(file))
            // 耗時(shí)循環(huán)體:大多數(shù)IO耗時(shí)操作都會(huì)有一個(gè)循環(huán)的
            while (isActive) {
                bufferedReader.readLine() ?: break
            }
            log("2")
        }
        delay(100L) // 等待一段時(shí)間
        job.cancel()
        log("3")
    }
}
// output
Thread[DefaultDispatcher-worker-1,5,main]1
Thread[main,5,main]3
Thread[DefaultDispatcher-worker-1,5,main]2
Thread[main,5,main]end
  • cancelAndJoin()是會(huì)等待耗時(shí)循環(huán)體后面的代碼執(zhí)行完成的,這個(gè)一般不會(huì)花太多時(shí)間,因?yàn)樽詈臅r(shí)的操作已經(jīng)通過判斷isActive跳過了。一般會(huì)使用這個(gè)函數(shù)去處理。如下代碼的輸出順序可證明該結(jié)論:
suspend fun test2() {
    coroutineScope {
        val job = launch(Dispatchers.Default) {
            log("1")
            val file = File("E:\\Opera_64.0.3417.54_Setup_x64.exe")
            val bufferedReader = BufferedReader(FileReader(file))
            while (isActive) {
                bufferedReader.readLine() ?: break
            }
            log("2")
        }
        delay(100L) // 等待一段時(shí)間
        job.cancelAndJoin()
        log("3")
    }
}
// output
Thread[DefaultDispatcher-worker-1,5,main]1
Thread[DefaultDispatcher-worker-1,5,main]2
Thread[main,5,main]3
Thread[main,5,main]end

可能到這里有人會(huì)說了,那cancelcancelJoin這兩個(gè)api有什么用呢?協(xié)作式的停止協(xié)程需要自己去寫判斷條件呀?我的回答是yes,你需要自己寫,不過幸運(yùn)的是,耗時(shí)操作一般是網(wǎng)絡(luò)+文件讀取,其實(shí)說白了就是IO,這兩種場景都有現(xiàn)成的框架,比如網(wǎng)絡(luò)有Retrofit等,這些經(jīng)典的框架對cancel這樣的操作是做了適配的。即使沒有做適配,你也可以自己簡單的寫一個(gè)帶取消協(xié)程。步驟如下:

  • 首先你需要理解suspendCancellableCoroutine:類似上篇講的suspendCoroutineUninterceptedOrReturn其回調(diào)也會(huì)返回一個(gè)續(xù)體
suspend fun test4() = suspendCancellableCoroutine<String> { cont ->
    // 定義一個(gè)取消回調(diào)事件
    cont.invokeOnCancellation {
        // 在這里可以做一些耗時(shí)操作的cancel處理
    }
}
  • OkHttp為例,就可以在invokeOnCancellation回調(diào)中調(diào)用call.cancel去真正的取消該請求。
suspend fun test4() = suspendCancellableCoroutine<String> { cont ->
    val call = OkHttpClient().newCall(...)
    // 定義一個(gè)取消回調(diào)事件
    cont.invokeOnCancellation {
        // 取消請求
        call.cancel()
    }
}

這就完成了,我們在主函數(shù)的調(diào)用代碼如下

val job = launch { //①
    log(1)
    val res = test4()
    log(res)
    log(2)
}
delay(10)
log(3)
job1.cancel()
log(4)

(附:Retrofit從2.6.0版,已經(jīng)對其做了適配, 也是類似上面的原理。后面的協(xié)程實(shí)踐篇我會(huì)詳細(xì)介紹)

上面可能表述有點(diǎn)亂,所以現(xiàn)在總結(jié)一下cancelcancelJoin()到底做了什么

小結(jié)

取消協(xié)程主要有兩個(gè)方法cancel()cancelJoin(),前者立即停止協(xié)程執(zhí)行并執(zhí)行后面代碼,后者需要等待協(xié)程執(zhí)行完成后在執(zhí)行后面的代碼。其中cancel主要做了兩件事

  • 狀態(tài)轉(zhuǎn)移:即設(shè)置狀態(tài)flag: isActive = false
  • 處理回調(diào):通知各個(gè)觀察者(訂閱者)
    join主要就是干了一件事,即等待設(shè)置了isActive=false的協(xié)程執(zhí)行完成。

思考與總結(jié)

很多對象的生命周期結(jié)束的問題會(huì)使程序設(shè)計(jì)和實(shí)現(xiàn)等過程變得復(fù)雜,比如在應(yīng)用層你可能需要在結(jié)束時(shí)釋放資源,在框架層你需要考慮內(nèi)部的狀態(tài)遷移和資源釋放的問題。但是該步驟又是非常重要的,比如在android開發(fā)中就可能因?yàn)榇嗽斐蓛?nèi)存泄漏等現(xiàn)象。所以在應(yīng)用層設(shè)計(jì)的時(shí)候一定要考慮取消的情況,本文開篇通過引入線程的取消機(jī)制(協(xié)作機(jī)制)為后文描述協(xié)程的取消做鋪墊,因?yàn)閮烧叻浅n愃疲?/p>

  • 線程的interrupt()等同于協(xié)程中的cancel()方法
  • 線程中判斷是否中斷的方法isInterrupted()等同于協(xié)程中的isActive

但是由于協(xié)作機(jī)制的復(fù)雜性(需要協(xié)作)導(dǎo)致在很多情況下需要自己在協(xié)程A中取消,在協(xié)程B中需要手動(dòng)判斷協(xié)程是否取消來跳過循環(huán)耗時(shí)函數(shù)。這就需要某些異步框架中對該機(jī)制做適配,比如RetrofitRxJava做了適配,當(dāng)然retrofit也對協(xié)程做了適配。對于沒有做適配的框架,本文也給出了一個(gè)的簡單demo自己去做適配。

最后略顯遺憾的是,沒有對cancel進(jìn)行源碼級的分析,后面有機(jī)會(huì)補(bǔ)上。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容