cancel
可以被取消的suspend函式
delay()、withContext()都可以被協程取消。
job取消協程
以下只會執行list[0]的job,因為運行1.1秒後,所有子協程全被取消。
cancel()是取消協程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
fun coroutin14() = runBlocking {
val job = Job()
val list = listOf(
launch(job) {
delay(1000)
println("list[0] finish")
},
launch(job) {
delay(2000)
println("list[1] finish")
})
job.start()
// 1.1秒後,取消所有子協程
delay(1100)
job.cancel()
list.forEach { it.join() }
println("all children finish.")
}
list[0] finish
all children finish.
join 與 cancel
以下的程式碼child.cancel()不會立刻馬上取消,而join會轉變成「等待」取消完成,確保child協程「執行完畢」。
以下不會有任何執行結果,因為0.1秒(100ms),就把child.cancel()。
1
2
3
4
5
6
7
8
9
10
11
12
13
fun coroutin08() = runBlocking {
val job = Job()
val child = launch(job) {
delay(1000)
println("child finish")
}
// 暫停0.1秒
delay(100)
// 取消協程
child.cancel()
// runBlocking等待完成取消
child.join()
}
如果只有cancel,協程正在清理資料,但runBlocking執行完了,就退出了。
job.cancel()
需要二者一起搭配。
1
2
3
4
// 取消協程
job.cancel()
// 等待
job.join()
也可以使用cancelAndJoin()取代。
1
job.cancelAndJoin()
delay()
delay() 是一個可取消的掛起函數,當協程被取消時,delay()會拋出 CancellationException。
但kotlin的CancellationException是被忽略,除非有try{…}catch{…},才能補捉到CancellationException。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
fun coroutin08() = runBlocking {
val job = Job()
val child = launch(job) {
try {
delay(1000)
println("child finish")
}catch (e: Exception) {
e.printStackTrace()
}
}
// 暫停0.1秒
delay(100)
// 取消協程
child.cancel()
// runBlocking等待完成取消
child.join()
}
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#3":StandaloneCoroutine{Cancelling}@ff684e1
作用域Scope 取消
下面的程式碼是,GlobalScope獨立作用域的取消。
取消協程不會「顯示」任何exception,但實際上會拋出CancellationException。
取消作用域Scope
作用域scope.cancel(),會直接把相同作用域的協程取消。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
fun coroutin19() = runBlocking {
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch {
delay(1000)
println("job1 finish")
}
val job2 = scope.launch {
delay(1000)
println("job2 finish")
}
delay(500)
scope.cancel()
job1.join()
job2.join()
}
取消子協程
以下job2仍會執行,因為只有取消job1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
fun coroutin19() = runBlocking {
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch {
delay(1000)
println("job1 finish")
}
val job2 = scope.launch {
delay(1000)
println("job2 finish")
}
delay(500)
// 只有取消job1
job1.cancel()
job1.join()
job2.join()
}
job2 finish
try catch CancellationException
cancel()取消時會有CancellationException,但不會在終端機輸出,因為對kotlin來說,這是正常的Exception。
加上try{}… catch{}就可以抓出CancellationException。
可以在cancel()傳入自訂例外的名稱。
job1.cancel(CancellationException("自訂取消Exception"))
job1.join()變成等待取消。
1
2
3
4
5
6
7
8
9
10
11
12
13
fun coroutin08() = runBlocking {
val job1 = GlobalScope.launch {
try {
delay(1000)
println("job1")
}catch (e: Exception) {
e.printStackTrace()
}
}
delay(100)
job1.cancel(CancellationException("自訂取消Exception"))
job1.join()
}
java.util.concurrent.CancellationException: 自訂取消Exception
at com.example.coroutine.Test01$coroutin08$1.invokeSuspend(Test01.kt:105)
finally
取消但一定會執行finally
不管有沒有被取消,都一定會執行finally{}。
下面程式碼,取消job1,job2沒取消,job1不會輸出”job1 finish”,但取消時會輸出”job1 finally”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
fun coroutin19() = runBlocking {
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch {
try {
delay(1000)
println("job1 finish")
} finally {
println("job1 finally")
}
}
val job2 = scope.launch {
try {
delay(1000)
println("job2 finish")
} finally {
println("job2 finally")
}
}
delay(500)
job1.cancel()
job1.join()
job2.join()
}
job1 finally
job2 finish
job2 finally
withContext(NonCancellable)
被cancel的協程中,在finally有suspend函式,delay()是suspend函式,不會執行。
以下child1被取消,不會印出「finally 2」。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
fun coroutin24() = runBlocking {
val parent = Job()
val child1 = launch(parent) {
try {
delay(1000)
println("child1 finish")
} finally {
println("finally 1")
delay(100)
println("finally 2")
}
}
val child2 = launch(parent) {
delay(1000)
println("child2 finish")
}
delay(500)
child1.cancel()
child1.join()
child2.join()
}
finally 1
child2 finish
改用withContext(NonCancellable)包住suspend函式就可以,系統會執行完withContext後才會取消完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
fun coroutin24() = runBlocking {
val parent = Job()
val child1 = launch(parent) {
try {
delay(1000)
println("child1 finish")
} finally {
withContext(NonCancellable) {
println("finally 1")
delay(100)
println("finally 2")
}
}
}
val child2 = launch(parent) {
delay(1000)
println("child2 finish")
}
delay(500)
child1.cancel()
child1.join()
child2.join()
}
finally 1
finally 2
child2 finish
Cpu運算無法被取消
以下程式碼調度器要用Dispatchers.Default,否則無法取消,Default是屬於CPU運算。
照理說,delay 0.1秒後,job1要被取消,但一直執行,i印到9。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun coroutin21() = runBlocking {
val job1 = launch(Dispatchers.Default) {
var nexTime = System.currentTimeMillis()
var i = 0
try {
while (i < 10) {
if (System.currentTimeMillis() >= nexTime) {
println("i = $i isActive = $isActive")
i++
// 每0.5秒循環一次
nexTime += 500
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
delay(100)
job1.cancel()
job1.join()
}
i = 0 isActive = true
i = 1 isActive = false
i = 2 isActive = false
i = 3 isActive = false
i = 4 isActive = false
i = 5 isActive = false
i = 6 isActive = false
i = 7 isActive = false
i = 8 isActive = false
i = 9 isActive = false
Job Cancel狀態
由下表可以知道Cancelling 取消中、Cancelled 取消完成、Completed 完成中,isActive都是false的狀態,所以可以利用isActive來判斷是否在取消中、取消完成。
| 狀態 | isActive | isCompleted | isCancelled |
|---|---|---|---|
| New 建立 | false | false | false |
| Active 執行中 | true | false | false |
| Completing 完成中 | true | false | false |
| Cancelling 取消中 | false | false | true |
| Cancelled 取消完成 | false | true | true |
| Completed 完成 | false | true | false |
下圖中,Completed完成,isCancelled是false。
取消中跟取消完成,isCancelled是false
取消中(Cancelling)、取消完成(Cancelled)、完成(Completed),三種狀態,isActive都是false。
會造成取消除了使用cancel(),協程拋出非正常Exception(排除CancellationException),都會進入到取消中(Cancelling)的狀態。

isActive判斷子協程是否被取消
isActive會傳回是否在取消。
若協程被取消,會傳回false。
以下程式3秒後,取消父親為job的所有子協程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Test
fun coroutin16() = runBlocking {
val job = Job()
val list = listOf(
launch(job) {
// isActive會傳回協程是否正在運行中
while (isActive) {
println("list[0] runing")
// 暫停1秒
delay(1000)
}
},
launch(job) {
// isActive會傳回協程是否正在運行中
while (isActive) {
println("list[1] runing")
// 暫停1秒
delay(1000)
}
})
job.start()
// 3秒後,取消父親為job的所有子協程。
delay(3000)
job.cancel()
list.forEach { it.join() }
println("子協程全被取消")
}
list[0] runing
list[1] runing
list[0] runing
list[1] runing
list[0] runing
list[1] runing
子協程全被取消
isActive
加上isActive判斷Job的狀態是否在取消中,若在取消中就不執行。
執行結果只印出i = 0,不會一直印出。
加上try … catch … 補捉CancellationException的例外。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
fun coroutin21() = runBlocking {
val job1 = launch(Dispatchers.Default) {
var nexTime = System.currentTimeMillis()
var i = 0
try {
while (i < 10 && isActive) {
if (System.currentTimeMillis() >= nexTime) {
println("i = $i isActive = $isActive")
i++
// 每0.5秒循環一次
nexTime += 500
}
}
} catch (e: CancellationException) {
println("補捉到CancellationException Exception")
}
}
delay(100)
job1.cancel()
job1.join()
}
i = 0 isActive = true
補捉到CancellationException Exception
ensureActive()
ensureActive()原始碼也是使用isActive,判斷Job狀態是不是取消中或取消完成。
1
2
3
public fun Job.ensureActive(): Unit {
if (!isActive) throw getCancellationException()
}
ensureActive()會拋出JobCancellationException。
1
public fun getCancellationException(): CancellationException
加上try … catch … 補捉CancellationException的例外。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
fun coroutin21() = runBlocking {
val job1 = launch(Dispatchers.Default) {
var nexTime = System.currentTimeMillis()
var i = 0
try {
while (i < 10) {
ensureActive()
if (System.currentTimeMillis() >= nexTime) {
println("i = $i isActive = $isActive")
i++
nexTime += 500
}
}
} catch (e: CancellationException) {
println("補捉到CancellationException Exception")
}
}
delay(100)
job1.cancel()
job1.join()
}
i = 0 isActive = true
補捉到CancellationException Exception
yield
yield()判斷Job狀態是不是取消中或取消完成,密集計算會佔用cpu資源,yield會讓出部分cpu資源給其它的Job使用,不會獨佔Cpu資源,讓出「部分」cpu資源,還是會把密集計算的程式碼完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun coroutin21() = runBlocking {
val job1 = launch(Dispatchers.Default) {
var nexTime = System.currentTimeMillis()
var i = 0
try {
while (i < 10) {
yield()
if (System.currentTimeMillis() >= nexTime) {
println("i = $i isActive = $isActive")
i++
nexTime += 500
}
}
} catch (e: CancellationException) {
println("補捉到CancellationException Exception")
}
}
delay(100)
job1.cancel()
job1.join()
}
i = 0 isActive = true
補捉到CancellationException Exception
超時處理
withTimeout(ms)
以下程式碼執行超過3秒就會被cancel()取消。
會產生TimeoutCancellationException。
1
2
3
4
5
6
7
8
fun coroutin25() = runBlocking {
withTimeout(3000) {
repeat(10) { i ->
println("i = $i")
delay(1000)
}
}
}
i = 0
i = 1
i = 2
Timed out waiting for 3000 ms
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 3000 ms
withTimeoutOrNull
超時就傳回null,沒有超時就傳回finish
1
2
3
4
5
6
7
8
9
10
11
12
@Test
fun coroutin25() = runBlocking {
val timeout = withTimeoutOrNull(3000) {
repeat(10) { i ->
println("i = $i")
delay(1000)
}
// 完成的文字
"finish"
}
println("result = $timeout")
}
i = 0
i = 1
i = 2
result = null