协程的启动模式

2,103 阅读7分钟

笔者最近一直在研究协程。至于协程是什么。不在本文的介绍范围。之后会专门说一下。协程与线程的区别。这篇文章主要还是介绍一下协程的启动模式。对,你没听错。协程也是有启动模式的

协程的构造方法

先看一下协程的构造方法,其第二个参数就是启动模式。而且还给了一个默认的模式 DEFAULT

public fun CoroutineScope.launch(
    //上下文调取器
    context: CoroutineContext = EmptyCoroutineContext,
    //启动模式
    start: CoroutineStart = CoroutineStart.DEFAULT,
    //协程方法
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

启动模式类型

点开CoroutineStart启动模式的枚举类型如下 默认就是DEFAULT

public enum class CoroutineStart {
    //协程创建后。立即开始调度。在调度前如果协程被取消。直接进入取消响应的状态
    DEFAULT,
    //当我们需要他执行的时候才会执行,不然就不会执行。
    LAZY,
    //立即开始调度,协程之前到第一个挂起点之前是不响应取消的
    ATOMIC,
    //创建协程后立即在当前函数的调用栈中执行。直到遇到第一个挂起点为止。
    UNDISPATCHED
}

要搞懂启动模式之前,需要有几个概念需要理解

  • 调度不代表执行。 这里的调度,说的其实是第一个参数。上下文CoroutineContext。暂时可以理解成一个队列,协程任务被添加到这个队列。不代表执行了。
  • 调度到执行之间是有时间的
  • 协程可以被取消

DEFAULT

查看DEFAULT。的官网介绍

协程创建后。立即开始调度。在调度前如果协程被取消。直接进入取消响应的状态

先发上一个笔者画的图。稍后来解释一下

协程创建后。立即开始调度这句话的理解。比较有意思。协程任务被添加入调度器,也就是之前说的队列。不需要去start() 或者 join()任务自行就会触发。下面的测试代码。是否开始start()对于测试结果来说是没有任务影响的。

在调度前如果协程被取消。直接进入取消响应的状态,这个更加有意思。首先创建一个协程任务并且添加到队列中。任务就会自行触发。cancel 的时间就很难把握,是在任务执行前呢,还是在执行后呢,即使创建协程以后。立马cancel,也有可能调度器已经在执行协程的代码块了。

在解释一下下面的测试代码,笔者为了获取多次结果。跑了100次,delay(100)是一个挂起函数,之后会说明。sleep(20) 是在执行方法的线程上。阻塞20毫秒。作用就是为了保证任务一定被添加到调度器中。

private fun todoModuleDefault(
      start: CoroutineStart = CoroutineStart.DEFAULT,
      isStart: Boolean = false
  ) {
      for (i in 1..100) {
          val buffer = StringBuffer()
          buffer.append("start")
          val job = GlobalScope.launch(start = start) {
              buffer.append("->执行协程")
              delay(100)
              buffer.append("->协程执行完成")
          }
          buffer.append("->启动协程")
          //是否start 不会影响结果
          if (isStart) {
              job.start()
          }
          //确保一定执行了 start()
          sleep(20)
          buffer.append("->取消协程")
          job.cancel()
          buffer.append("->end")
          println("第${i}$buffer")
      }
  }

运行结果如下。启动协程执行协程的先后顺序会发生变化。如同我们上面说的一样。协程任务添加到调度器之后。会立马执行。才不会管你有没有 start() 而谁先谁后完全取决于当时的调度器状态。无法控制。

cancel()方法,在20毫秒之后执行。而此时的协程已经被挂起了。delay(100)。这时候cancel(),就没有在打印 协程执行完成。也证实了协程是可以被取消的。

13次 start->启动协程->执行协程->取消协程->end
第17次 start->执行协程->启动协程->取消协程->end

LAZY

LAZY

当我们需要他执行的时候才会执行,不然就不会执行。如果调度前就被取消。那么直接进入异常结束状态

所谓的需要他执行的时候才会执行,就是执行了 start()或者 join() 才会启动。将上面的测试代码修改成 LAZY 模式。此时isStart变量改成true执行结果如下

start->启动协程->执行协程->取消协程->end

改成false 不执行start()的测试结果如下

start->启动协程->取消协程->end

很明显。是否执行协程。完全取决于我是否start()

如果调度前就被取消。那么直接进入异常结束状态,就是说将协程任务添加到调度器中。等待被执行。此时将去取消。那么协程是不会在执行的。

🌰举个例子

看一下下面的示例。故意cancel()之后再去start()

private fun todoModuleLazyCancel(
    start: CoroutineStart = CoroutineStart.LAZY
) {
    for (i in 1..100) {
        val buffer = StringBuffer()
        buffer.append("start")
        val job = GlobalScope.launch(start = start) {
            buffer.append("->执行协程")
            delay(100)
            buffer.append("->协程执行完成")
        }
        buffer.append("->取消协程")
        job.cancel()
        //确保一定执行了 cancel()
        sleep(20)
        buffer.append("->启动协程")
        job.start()
        buffer.append("->end")
        println("第${i}$buffer")
    }
}

执行结果,协程不会在被执行了。

start->取消协程->启动协程->end

ATOMIC

ATOMIC

立即开始调度,协程之前到第一个挂起点之前是不响应取消的

什么叫做挂起点呢,就是执行挂起函数的地方,也就是执行suspend修饰过的方法函数。在示例中 delay 就是一个挂起函数

他和DEFAULT有点像。都是直接开始调度。不用去start()或者join()

不同点就是在到第一个挂起点之前是不响应取消的,而DEFAULT就没这个要求,随时可以取消。

🌰举个例子

将模式改成ATOMIC,是否start()不会影响结果

private fun todoModuleAtomic(
    start: CoroutineStart = CoroutineStart.ATOMIC,
    isStart: Boolean = false
) {
    for (i in 1..100) {
        val buffer = StringBuffer()
        buffer.append("start")
        val job = GlobalScope.launch(start = start) {
            buffer.append("->执行协程")
            delay(100)
            buffer.append("->协程执行完成")
        }
        buffer.append("->启动协程")
        //是否start 不会影响结果
        if (isStart) {
            job.start()
        }
        //确保一定执行了 start()
        sleep(20)
        buffer.append("->取消协程")
        job.cancel()
        buffer.append("->end")
        println("第${i}$buffer")
    }
}

执行结果如下。因为在第一个挂起点之前不响应取消cancel()所以也就决定了。协程一定会执行

2021-01-19 20:50:58.780 I: 第3次 start->启动协程->执行协程->取消协程->end
2021-01-19 20:50:58.982 I: 第4次 start->执行协程->启动协程->取消协程->end

UNDISPATCHED

创建协程后立即在当前函数的调用栈中执行。直到遇到第一个挂起点为止。

乍一看感觉和ATOMIC很像。注意了。这里是在当前函数的调用栈,啥意思呢?就是在你执行协程的线程。例如下面的示例。我实在主线程上执行的。那么在第一个挂起点之前。也就是 delay(20)之前。也是在主线程。而ATOMIC就是在调度器所创建的线程。

private fun todoModuleUnDispatched(
    start: CoroutineStart = CoroutineStart.UNDISPATCHED,
    isStart: Boolean = false
) {
    for (i in 1..100) {
        val buffer = StringBuffer()
        buffer.append("start")
        val job = GlobalScope.launch(start = start) {
            buffer.append("->执行协程A")
            buffer.append("[${Thread.currentThread().name}]")
            delay(20)
            buffer.append("->执行协程B")
            buffer.append("[${Thread.currentThread().name}]")
            delay(100)
            buffer.append("->协程执行完成")
        }
        buffer.append("->启动协程")
        //是否start 不会影响结果
        if (isStart) {
            job.start()
        }
        //确保一定执行了 start()
        sleep(50)
        buffer.append("->取消协程")
        job.cancel()
        buffer.append("->end")
        println("第${i}$buffer")
    }
}

UNDISPATCHED模式下的日志。在主线程

start->执行协程A[main]->启动协程->执行协程B[DefaultDispatcher-worker-1]->取消协程->end

LAZY模式下的日志,在后台线程。

start->启动协程->执行协程A[DefaultDispatcher-worker-2]->执行协程B[DefaultDispatcher-worker-1]->取消协程->end
start->启动协程->执行协程A[DefaultDispatcher-worker-1]->执行协程B[DefaultDispatcher-worker-1]->取消协程->end 

还有一点。因为UNDISPATCHED创建协程后立即在当前函数的调用栈中执行。所以他的协程也一定会执行。所以啊。在这4中模式中LAZYUNDISPATCHED是一定执行协程的两种总启动模式。

最后

协程是个挺有意思的东西。笔者这里可能理解不全面。有错误。请指正。错误的文章会误导更多的人。

笔者之前说调度器是一个队列。就不准确。不过是为了让人更清楚理解每种启动模式。我想到的一个替代品.切不可直接就任务调度器CoroutineContext就是队列。

好了以上就是本期带来的 协程的启动模式。

参考

破解 Kotlin 协程(2) - 协程启动篇