任何對象都有生命周期,協(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é)程的取消前,先來看線程是如何取消的。
如何取消線程
-
stop和suspend:在底層上存在嚴(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é)程是否取消。
cancel與cancelAndJoin()
-
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ì)說了,那cancel和cancelJoin這兩個(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é)一下cancel和cancelJoin()到底做了什么
小結(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ī)制做適配,比如Retrofit對RxJava做了適配,當(dāng)然retrofit也對協(xié)程做了適配。對于沒有做適配的框架,本文也給出了一個(gè)的簡單demo自己去做適配。
最后略顯遺憾的是,沒有對cancel進(jìn)行源碼級的分析,后面有機(jī)會(huì)補(bǔ)上。