Kotlin学习系列之:协程的创建(三)

x33g5p2x  于2022-03-08 转载在 其他  
字(4.0k)|赞(0)|评价(0)|浏览(1040)

经过前面两篇的学习,我们现在可以来总结一下,我们可以有哪些方式来启动一个协程:

  • GlobalScope.launch{}
  • runBlocking{}
  1. 接下来我们介绍另外的两种方式,我们先直接来看代码:
fun main() = runBlocking {

//    GlobalScope.launch {
//        delay(1000)
//        println("hello coroutine!")
//    }

    launch {
        delay(1000)
        println("hello coroutine!")
    }

    println("hello")
    delay(2000)
    println("world")

}

我们将之前的GlobalScope.launch{}的方式,可以直接替换成launch{}。一运行,我们能够得到相同的输出结果:

0s: 打印hello,runBlocking协程开始delay;启动一个子协程(launch{}起的作用),并且也开始delay

1s: launch子协程delay时间到,打印hello coroutine!

2s: runBlocking协程delay时间到,打印world

那么这个launch方法是何方神圣呢?我们通过command键+鼠标进入到它的方法声明处,你会发现,它和GlobalScope.launch{}的一样的,都是CoroutineScope的扩展方法。GlobalScope.launch{}的调用对象是GlobalScope,那么这里的launch{}方法的调用对象是谁呢?

这是IntelliJ Idea编辑器给我们的提示,就是在runBlocking{}代码块中会有一个CoroutineScope的对象作为this,实际上不光光是runBlocking{},细心的你肯定会发现上面截图中的launch{}代码块中一样有这样的this。至于是为什么,我们来看官方文档的描述:
Every coroutine builder, including runBlocking, adds an instance of CoroutineScope to the scope of its code block. 

翻译过来就是:对于每一个协程构建器而言,包括runBlocking,都会往它的代码块的作用域中添加一个CoroutineScope的实例。换句话说,这个CoroutineScope的实例是由协程构建器注入的。

  1. 接下来我们来比较一下GlobalScope.launch{}和.launch{}的不同之处:我们将runBlocking{}中delay(2000)修改成delay(500),然后观察分别使用GlobalScope.launch{}和.launch{}的运行结果.
fun main() = runBlocking {

   GlobalScope.launch {
       delay(1000)
       println("hello coroutine!")
   }

   println("hello")
   delay(500)
   println("world")
}

hello

(暂停500毫秒)

world

我们会发现,程序在输出完”world“之后,就退出了。GlobalScope.launch{}中的”hello coroutine“压根就没有机会输出。

fun main() = runBlocking {

   launch {
       delay(1000)
       println("hello coroutine!")
   }

   println("hello")
   delay(500)
   println("world")
}

hello

(暂停500毫秒)

world

(暂停500毫秒)

hello coroutine!

可以看到,使用.launch{}后,在程序输出完world之后,还会继续等待launch{}代码块中的执行,也就是会输出”hello coroutine!“,然后才退出程序。

好,现象我们已经看到了,下面来剖析原因:
Every coroutine builder, including runBlocking, adds an instance of CoroutineScope to the scope of its code block. We can launch coroutines in this scope without having to join them explicitly, because an outer coroutine(runBlocking in our example) does not complete until all the coroutines launched in its scope complete.

第一句话我们前面已经翻译过了,我们来看后面的:

我们可以不需要显式地join时,在这个作用域下启动一个协程,因为外部的协程会等待在其作用域下启动的所有协程执行完毕后,才标志着自己地执行完毕。

实际上这里的协程我们可以称作是父子协程(外部是父,内部是子),我们后面还会有专门的篇幅来探究父子协程之前的取消、协作等关系。

  1. 下面我们来介绍第四种启动协程的方式:coroutineScope{}
fun main() = runBlocking {
   println("hello")
   coroutineScope {
      delay(1000)
      println("welcome")
   }
   delay(500)
   println("world")
}

hello

(暂停1000毫秒)

welcome

(暂停500毫秒)

world

这里的输出结果我们先不去探究,我们来看这个coroutineScope{}是个啥:

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R =
   suspendCoroutineUninterceptedOrReturn { uCont ->
       val coroutine = ScopeCoroutine(uCont.context, uCont)
       coroutine.startUndispatchedOrReturn(coroutine, block)
   }

这里会有一个之前没有接触过的关键字:suspend.对于使用suspend关键字修饰的函数我们称之为挂起函数(Suspend Function),它有如下特点:它只能用于协程作用域下或者是另一个挂起函数中。比如之前我们一直使用的delay方法,它就是一个挂起函数。再来看看这个coroutineScope的文档描述:

* Creates a [CoroutineScope] and calls the specified suspend block with this scope.
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
* the context's [Job].
*
* This function is designed for _parallel decomposition_ of work. When any child coroutine in this scope fails,
* this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]).
* This function returns as soon as the given block and all its children coroutines are completed.
* A usage example of a scope looks like this:
*
* ```
* suspend fun showSomeData() = coroutineScope {
*
*   val data = async(Dispatchers.IO) { // <- extension on current scope
*      ... load some UI data for the Main thread ...
*   }
*
*   withContext(Dispatchers.Main) {
*     doSomeWork()
*     val result = data.await()
*     display(result)
*   }
* }

大家可以通读一下,我这里就重点关注一句话:

This function returns as soon as the given block and all its children coroutines are completed.

就是说这个函数会在内不能的代码块和所有的子协程都执行完毕后才会返回。有了这句话,我们就能够轻松解释我们前面示例中的输出结果。当程序执行到coroutineScope一行之后,会等待其中的子协程和代码块执行完毕后,才会走下面的执行逻辑,所以下面delay(500)以及println("world")都只会在后面得到执行。

4. 我们再次总结一波启动协程的方式:

  • GlobalScope.launch{}: 创建一个全局的协程
  • runBlocking{}:会阻塞当前线程,一般用于代码调试中
  • .launch{}: 和GlobalScope.launch{},只不过它是依赖于当前协程作用,而GlobalScope.launch{}是当前线程的全局作用域
  • coroutineScope{}: 称之为作用域构建器(scope builder),它是一个挂起函数,它会等待其作用域下的所有代码块以及子协程的执行完毕后才会返回。

相关文章