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)的狀態。

img

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

results matching ""

    No results matching ""