• 什么是结构化并发?
  • 说好的反常传达为啥失效了?
  • 怎样还有async不抛反常的问题?

1 结构化并发(Structured Concurrency)

1.1 java的”离散性并发”

kotlin 的Coroutine是【结构化并发】,与结构化并发对应的办法是【fire and forget 】权且称之为【离散性并发】吧,或许不太准确。一个比如解释下离散性并发,java里咱们开启一个线程之后,是不具有盯梢办理这个线程的能力的。如下

  public void javaThreadFun() {
    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        //do some work
       }
     });
    thread.setName("child-thread");
    thread.start();
   }

这个比如中,调用javaThreadFun()办法所在的线程,创建并发动child-thread线程之后两个线程没有清晰的父子联系,avaThreadFun()办法所在的线程不能天然的感知在自己线程里发动的”子线程”,子线程发生反常之后也不会影响到自己。假如父线程要撤销中止在自己线程里发动的那些线程也没有现成的办法去供运用。总归,层级联系办理上很离散。

1.2 kotlin 协程的的结构化并发

2023也该知道了:kotlin协程取消和异常传播机制

kotlin的协程天然的具有父协程办理撤销子协程、子协程的反常失利影响父协程或许父协程感知子协程过错和失利的能力。如下示例

   GlobalScope.launch {
      val parentJob = launch {
        val childJob = launch {
          delay(1_000)//子使命做一些工作
          throw NullPointerException() //会导致父协程使命和兄弟协程使命都会被撤销
         }
        delay(5_000)
       }
     }
  • childJob失利抛出反常,会影响到父job,从而父job会撤销掉其一切的子job。
  • 别的,父job也会等候一切的子使命完毕后自己才会完毕。

与传统的相比

  • 有盯梢:在协程左右域里发动协程会作为该协程的子协程,该协程会盯梢这些协程的状况。而不是像线程那些开启之后就忘掉没有盯梢。父协程的完毕也是要在一切子协程都完成之后自己才会完成,颇有家长负责制的感觉。
  • 可撤销:撤销父协程也会,把其子协程同时撤销掉。如上图,撤销掉parent-job会导致从属于他的一切子协程撤销。
  • 能传达:这特性体现在,子协程发生反常,会告诉其父协程,父协程会撤销掉自己一切的子协程然后再向上传递直到根协程,或许supervisorJob.(这个下文咱们会打开剖析)

2 撤销机制

2.1 父协程的撤销会撤销子协程

这一章节咱们打开聊下Kotlin协程的撤销机制,上一节咱们提到,父协程/效果域的撤销也会撤销其子协程咱们看个比如。

 GlobalScope.launch {
    val mParentJOb: Job = this.launch {
      val child1Job: Job = this.launch {
        this.launch {
          delay(300)
         }.invokeOnCompletion { throwable ->
          println("child1Job 履行完毕,收到了${throwable}")
         }
        val child2Job = this.launch {
          delay(500)
         }.invokeOnCompletion { throwable ->
          println("child2Job 履行完毕,收到了${throwable}")
         }
       }
      delay(100)
     }
    mParentJOb.invokeOnCompletion { throwable ->
      println("mParentJOb 履行完毕,收到了${throwable}")
     }
    println("主张撤销 mParentJOb")
    mParentJOb.cancel()
   }.join()

运行结果:

主张撤销 mParentJOb
child1Job 履行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@100b06de
child2Job 履行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@100b06de
mParentJOb 履行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@100b06de

2.2 兄弟协程撤销不影响

private suspend fun brotherCoroutine() {
  coroutineScope {
    launch {
      delay(500)
      println("is running")
     }
    launch {
      delay(100)
      cancel()
     }.invokeOnCompletion {
      println("job2 is canceled")
     }
   }
}

这好像没有什么可解释的,某个协程的撤销并不会影响到其兄弟协程。

2.3 携程的撤销是协作式的

协程的撤销是协作式的体现在,对撤销的告诉需求自动的需求自动嗅探感知做出处理。举个比如

private suspend fun coroutineCanceling() {
  coroutineScope {
    val job = launch {
      var i = 0
      while (true) {//1
        println(" is running ${i++}")
       }
     }
    job.invokeOnCompletion {
      println("job is completion ${it}")
     }
    delay(50)
    job.cancel()
   }
}

会发现上面这个段代码并不能被撤销,原因便是协程并没有感知到自己现已被撤销了。这一点跟java thead interrupt机制相似,需求咱们感知撤销。感知撤销的办法有

  • 能够运用CoroutineScope.isActive()的办法check是否现已被撤销做出反响,代码一处可改成while (isActive)
  • 一切的suspend办法内部也会感知cancel。比如delay()办法便是一个suspend办法。

2.4 做好善后撤销

协程撤销后咱们或许会做一些诸如收回资源的动作,但在一个现已处于撤销状况的协程里再调用suspend办法就抛出CancellationException反常。此时咱们要运用 withContext(NonCancellable) 做撤销后的工作

private suspend fun handleCanceling() {
  coroutineScope {
    val job = launch {
      try {
        delay(100)//do Something
       } finally {
        withContext(NonCancellable) {
          delay(100)
         }
       }
     }
    job.invokeOnCompletion {
      println("job is completion ${it}")
     }
    delay(50)
    job.cancel()
   }
}

别的,还有特别留意的一点是,被撤销的协程会向外抛出反常假如运用try-catch捕获但不抛出反常CancellationException,会影响到反常的传达,也就损坏了协程的反常传达机制,详细下一节反常传达机制打开。

2.5 kotlin协程的父子结构

看下面这段代码,思考一个问题,2处字符串会被打印出出来吗,为什么?

private suspend fun parentChildStructTest() {
  coroutineScope {
    val job1 = launch {
     val job2 = launch(Job()) {//1
        delay(500)
        println("job2 is finish")//2
       }
      delay(100)
      this.cancel()
     }
   }
}

不会打印,不知道你有没有答对。

不是说好的,撤销父协程的时分会撤销掉其子协程吗?并且子协程里还调用了delay()办法,也会呼应撤销。问题的关键点在于,job1和job2的父子结构被损坏了。示例代码里1处传入了一个Job目标,此时job2的父层级现已变成了传入的job目标。咱们稍加改造下,这儿仅仅为了了解,不主张这么用,会发现job2能够被撤销了。

private suspend fun parentChildStructTest() {
  coroutineScope {
    val job1 = launch {
      val j = Job()
      val job2 = launch(j) {
        delay(500)
        println("job2 is finish")
       }.invokeOnCompletion {
        println("job2 OnCompletion $it")
       }
      delay(100)
      j.cancel() //1
     }
   }
}

新协程的context的组成有两个公式

parentContext = scopeContext + AddionalContext(launch办法传入的context)
​
childContext = parentConxtext + job(新建)

2023也该知道了:kotlin协程取消和异常传播机制
(图来自[Roman Elizarov])

  • 新协程的context是【parent context】和【新建job】的相加操作而来。
  • 【parent context】是由父层级的context和传入的参数context相加操作而来。
  • 子协程的job会和父层级中context的job建立一个父子联系。

当咱们运用coroutineScope.launch(Job()){}传入了一个job实例的时分,其实子协程的job和传入的job实例建立了父子结构,损坏了原本的父子结构。

3 反常传达机制

3.1 反常的传达

private suspend fun destroyCoroutineScope() {
  coroutineScope {
    launch {
      launch {
        delay(500)
        throw NullPointerException()
       }.invokeOnCompletion {
        println("job-1 invokeOnCompletion $it")
       }
​
      launch {
        delay(800)
       }.invokeOnCompletion {
        println("job-2 invokeOnCompletion $it")
       }
     }.invokeOnCompletion {
      println("job-parent completion $it")
     }
   }
}
  • 子job反常后,传达到父协程,父协程会撤销到自己一切的子协程,然后再往上传达
  • 假如是一个撤销反常(CancellationException)并不会被撤销协程,父协程的处理器会忽略他。也便是在子协程上抛出反常之后,父协程接收到不会做处理。

3.2 监督效果域反常传达(Supervision)

基本体现:运用supervisorScope发动的子协程发生反常时,不影响父协程和兄弟协程。

private suspend fun supervisorJobTest() {
  supervisorScope {
    launch {
      delay(100)
      throw NullPointerException()
     }
    launch {
      delay(800)
      println("job 2 is running")
     }
   }
}

如上代码,supervisor范围内第一个job抛出反常后,并不会影响第二个job;把过错反常控制在范围内。

  • SupervisorCoroutine的子协程发生了反常之后不会影响父协程本身,也不会向上传达。
  • 假如 CoroutineContext没有设置CoroutineExceptionHandle,终究反常会传达到ExceptionHandler

但其他的结构化并发特性依然存在

  • 当父协程撤销,他的协程也被撤销。
  • 子协程撤销不影响父协程。
  • 父协程抛出反常,子协程也会被撤销。
  • 父协程要等一切子协程完成后才完毕。

简略的讲,监督协程具有单向传达的特性,即子协程的反常和撤销不影响父协程,父协程的反常和撤销会影响子协程

两种办法:

  • 构建CoroutineScope时传入SupervisorJob()
  • 运用supervisorScope{}发生

留意

监督协程中的每一个子作业应该经过反常处理机制处理本身的反常。假如不处理反常会被吞掉。

3.3 CoroutineExceptionHandler

用于捕获协程履行过程中未捕获的反常,被用来定义一个大局的反常处理器。

  • 不能康复反常,仅仅打印、记录、重启应用。
  • 只能在【根效果域】或许【supervisorScope的直接子协程】发动协程是传入才生效。

举个比如

suspend fun coroutineExceptionHandlerTest() {
  supervisorScope {
    val handler = CoroutineExceptionHandler { _, _ -> println("handleException in coroutineExceptionHandler") }
    launch(handler) {
      delay(100)
      throw NullPointerException()
     }
   }
} 

3.4 浅看源码

主从效果域和协作效果域的体现差异上文现已讲到了,一般咱们构建一个协程效果域两种办法

val scope = CoroutineScope(Job())
val supervisorJob = CoroutineScope(SupervisorJob())
  • CourotineScope()办法(没错这是个办法),经过传入Job()SupervisorJob生成的目标终究获得主从效果域和协同效果域。
 supervisorScope { scope -> xx }
 coroutineScope { scope ->xx  }
  • 经过supervisorScope()或许coroutineScope()构建效果域。
private class SupervisorCoroutine<in T>(
  context: CoroutineContext,
  uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
  override fun childCancelled(cause: Throwable): Boolean = false
}
​
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
  override fun childCancelled(cause: Throwable): Boolean = false
}
​

两种效果域在代码上的差异是 fun childCancelled(cause: Throwable) 办法的实现不同,监督效果域直接回来fasle表明不处理子协程的过错反常。让其自己处理

//JobSupport
  private fun cancelParent(cause: Throwable): Boolean {
        ... 
    return parent.childCancelled(cause) || isCancellation //1
   }
​
  private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? {
      ...
    val handled = cancelParent(finalException) || handleJobException(finalException)//2
    if (handled) (finalState as CompletedExceptionally).makeHandled()
    ...
   }

源代码中的核心逻辑,

  • 1处的parent.childCanceled的值的终究来源其实便是咱们实现的childCancelled办法的回来值
  • 2处当咱们是一个监督效果域起cancelParent的回来值为false,这种状况下代码就会履行后半句handleJobException(),这半句的内部其实终究是履行了咱们设置的CoroutineExceptionHandler。
  • 2处cancelParent除了在咱们监督效果域的时分回来fasle,在根协程下会回来fasle,这也便是为什么CoroutineExceptionHandler设置在根协程下生效的原因。

代码很多细节不打开有爱好的自行研讨。

4 反常传达需留意问题

4.1 supervisorScope的孙子协程

private suspend fun childChildSupervisorJob() {
  supervisorScope { // SupervisorCouroutine
    launch { // ScopeCoroutine
      val job1 = launch {
        delay(100)
        throw NullPointerException()
       }
      val job2 = launch {
        delay(800)
        println("job 2 is running")
       }.invokeOnCompletion {
        println("job2 is completion $it")
       }
     }
   }
}
  • 看上面这个比如job1抛出空指针反常后,job2会不会受影响。
  • 是正常的coroutineScope而非supervisorScope,因此supervisorScope的“孙子协程”不遵循互不影响原则

4.2 留意不要损坏父子结构

private suspend fun textSupervisorJob() {
  supervisorScope {
    launch(SupervisorJob()) {//1
      launch {
        delay(100)
        throw NullPointerException()
       }
      launch {
        delay(800)
        println("job 2 is running")
       }.invokeOnCompletion {
        println("job2 is completion $it")
       }
     }
   }
}
  • job1抛出反常也会影响job2,原因1处尽管传入了SupervisorJob,可是这个实例其实是作为父context的job传入的,真是job1和job2的parentContext仍是job类型,而不是SupervisorJob。详细原理能够看2.5小节

5 关于async的误解

一般构建一个协程除了运用CoroutineScope.launch{}还会运用CoroutineScope.async{}。

经常看到这种说法,async办法发动的协程回来一个Deferred目标,当调用deffered的await()办法的时分才会抛出反常

private suspend fun asyncSample() {
  val h = CoroutineScope(CoroutineExceptionHandler { _, _ -> println("发生了反常") })
  val d = h.launch {
    async {
      delay(100)
      throw NullPointerException()
     }
    launch { //job2
      delay(500)
      println("job 2 is finish")
     }
   }.join()
}

这个比如没有调用await(),实际发现也会立马抛出反常,导致jo2都没履行完。跟咱们认为的不一样。

实际状况是这样的:当async被用作构建根协程(由协程效果域直接办理的协程)或许监督效果域直接办理协程时,反常不会自动抛出,而是在调用.await()时抛出。其他状况不等候await就会抛出反常。

6 总结

本文梳理了Kotlin的协程的撤销和反常传达处理机制。机制的设置总的来说是服务于结构化并发的。本文应该能让咱们了解把握以下问题才算合格

  • kotlin协程结构化并发的特性
  • 协程的context是怎样来的?怎样构成的?父协程的context和协程的parentContext是同一个概念吗?
  • kotlin的协程是怎样传达的?主从效果域监督效果域的差异?怎样实现
  • async办法发动的协程要await()的时分才抛出反常?