Kotlin Coroutine(一)

  从暑假开始学协程相关的东西。刚开始看一脸懵逼,之后重复好像看懂了->诶这个是怎么回事->再看看->好像看懂了->诶这个是咋回事……的循环,现在差不多终于明白了。记下笔记。
  

一点帮助理解的内容

  可以把协程看做一个缩写,全称协作式多例程/多任务(cooperative routine)。看的很多文章把协程和线程对比,是有道理的,对比的是线程的抢占式多任务。也就是说,协程其实是一种多任务调度方式,
  这就意味着线程和协程本质上是没法互操作的。毕竟两者的调度方式完全不一样。协程只能和协程之间进行调度。这也是为什么Kotlin中suspend方法只能由suspend方法调用。所以就不要想着从普通的函数中获取suspend函数的结果了。
  但是现在并没有直接支持协程的计算体系。所以承载协程的仍然是线程。刚开始的时候非常奇怪协程到底是怎么执行的,所谓的“暂停”到底是什么,为什么不会阻塞执行线程,所谓“一个协程”到底指的是个什么样的实体。我们一个一个来看。

协程如何创建/启动

  其实很简单。“协程”这个东西是由某个库函数创建的。在Kotlin中的API是launchlaunch中会调用(suspend ()->T).startCorountine(),最终调用到createCorountineUnchecked。真正的代表协程的对象Continuation在这个函数里被创建出来。当然这个里面做了一些非常magic的事,其中还需要编译器的帮助。更详细的后面再说。
  然后这个Continuation代表的就是传给launch的那整个lambda的代码,或者说是一次的完整执行流程,这一整个就是一个“协程”。更明确一点,在这个lambda里调用其他的suspend方法,以及被调用的suspend方法里继续调用的其他suspend方法,都是同一个协程/Continuation

协程何时会暂停

  官方文档这里挺坑的,说是在suspend方法里调用其他的suspend方法会导致当前的协程被暂停。导致我刚开始以为是每个suspend方法就被被包成一个Continuation。其实并不是字面上这样。这个暂停并不是发生在另一个suspend函数被调用的时候,而是发生在那个函数返回的时候。这里要牵扯到协程被转换为状态机的东西,暂时按下不表。总之关键在于一个suspend函数不会像代码写的那样执行和返回。最典型的是suspend suspendCoroutine(block: (Continuation) -> T): T这个函数。从方法名上它会把当前的执行流暂停,把表示当前执行流的Continuation对象捕获,然后传给block。但其实不一定会导致暂停。就像前面所说的,其实取决于suspend方法是怎么返回的。如果在block里面调用了传入的Continuation的resume或者resumeWithException,那么这个方法返回的时候会返回到原来的函数中,继续执行流(或者是抛出异常),返回值(或者异常)是resume/resumeWithException的参数。示例:

  suspend fun foo() {
      val a = suspendCoroutine { cont ->
          cont.resume(1)
      }
  }

  但是如果没有调用任何一个即返回,那么就会导致当前执行流被“暂停”。示例

    suspend fun foo() {
        val a = suspendCoroutine { cont ->
            someAsyncOperation { cont.resume(1) }
        }
    }

  someAsyncOperation会立即返回,一段时间后再回调cont.resume(1)这一句。所以,在suspendCoroutine返回时,resume尚未被调用,所以就导致了当前的执行流被“暂停”。
  自己写的方法是不可能暂停协程的,一定要调用suspendCorontine系的几个方法才有可能办得到。原因会在后面说。

暂停是一种什么状态

  suspendCorontine的源码没法告诉我们答案,它调用了suspendCorontineOrReturn这个没有Kotlin源码的方法(没有源码的一部分原因后面解释)。所以准确的答案不知道,我只能推测个大概。先从执行流的角度说。被暂停的协程会导致最开始的launch直接返回。对,也就是说launch里面调用了f1,f1里面调用了f2,f2再到f3……直到某一个被调用的suspend函数(假如叫fn)里调用了suspendCoroutine,而且在suspendCoroutine里直接返回了,那fn里suspendCorountine后面的代码、fn-1中调用fn后面的代码……f1中调用f2之后的代码和launch中调用f1之后的代码,都暂时不会被执行,直接跳过了。launch会直接返回,去执行launch之后的代码。
  那么剩下的代码都在哪里呢?都在Continuation里。编译器的魔法施展在这里,会把协程编译成一个状态机然后放在Continuation里,所以Continuation包含了整个协程的代码。它的引用正被持有在那个回调函数里(captured lambda)。等到异步操作完成,调用回调函数,回调函数中调用resume,也就对应着原来的执行流的继续。
  那么如果一直没用调用resume呢?Continuation也是普通的对象,所以迟早会被GC掉。这里资源泄露的问题看github上的design document。

在哪里resume

  也就是说被暂停的协程解除暂停后会在哪个线程上被继续执行?如果没有特定的interceptor,就会普通地在resume方法被调用的线程上被执行,也就是在异步操作完成后调用回调方法的那个线程上。
  当然这个行为可以定制。库中给我们准备好的方式就是用CoroutineDispatcher,并把它作为Context在launch的时候传进来。它继承自CoroutineInceptor,库进行了进一步的封装,所以一般只需要重载dispatch方法即可(needDispatch官方推荐不重载,保持默认的始终返回true的行为)。封装十分贴心,把resume和其他一些东西直接包成Runnable,你只需要把这个Runnable放到某个线程执行就行了。所以其实和线程池很相似,库里也因此提供了Executor.asDispatcher的拓展方法。
  顺便一提,Vertx提过了一个dispatcher,作用就是让协程始终保持在event loop上执行。所以如果不需要这个效果,可以不使用。

你可能感兴趣的:(Kotlin)