创建协程的方式主要有:launch()、async()、coroutineScope()、runBlocking() 。

launch()

创建一个异步协程(非阻塞),返回一个不带返回值的 job。

fun main(){
	GlobalScope.launch{// 默认 CorutineDispatcher 为 Dispatchers.Default
		delay(1000) // 仅可用于协程,而非线程
		println("Halloween Special") // 1
	}
  println("Happy") // 2
  Thread.sleep(2000)
  println("Halloween") // 3
}
  1. 由于这是在异步协程中进行的,而且延迟了 1 秒,所以此句会在 1 秒后打印
  2. 此句最先打印
  3. 此句延迟两秒,所以在 Kotlin Coroutiine 后打印。

输出结果为:

Happy
Halloween Special
Halloween

需要注意的是,当前线程是主线程,如果主线程执行完毕协程都还没有执行,那么协程将被销毁。因此如果将 Thread.sleep(2000) 改为 Thread.sleep(400),那么 Halloween Special 将不会打印,因为协程没有机会执行(它所依附的线程已经结束)。

上面的代码如果换成线程,则可以写成这样:

import kotlin.concurrent.thread
fun main(){
	thread { // thread 函数创建线程,前面的所有参数都采用默认值
 Thread.sleep(1000) // 仅可用于线程,而非协程
		println("Halloween Special") 
}
println("Happy") 
Thread.sleep(2000)
println("Halloween") 
}
runBlocking()

创建一个协程,并阻塞当前线程。直到协程结束。

fun main(){
	GlobalScope.launch {
    delay(1000)
    println("Halloween Special") 
  }
  println("Happy") 
  runBlocking{ // 创建新协程,并阻塞当前线程
    delay(2000)
  } 
  println("Halloween")
}

上述代码实现了和上节示例代码一样的效果,打印顺序相同,但没有再使用 Thread.sleep。换句话说,我们用 delay 代替了 Thread.sleep。

runBlocking 在 main 方法上有特殊的用法:

fun main() = runBlocking { // 1
	GlobalScope.launch {
		delay(1000)
    println("Halloween Special") 
	}
  println("Happy")
  delay(2000) // 2
  println("Halloween")
}

结果仍然和之前一样。和之前代码唯一的区别在于 1 和 2 处。当我们将 一个 runBlocking 协程直接赋值给 main 函数之后,我们就可以在尾随闭包中,使用 delay 函数而非 Thread.sleep 函数了。这说明 main 函数其实没有函数体, runBlocking 后面的 lambda 表达式其实是 runBlocking 的参数(尾随闭包的写法)而非 main 函数的函数体,因为这个尾随闭包是在一个 CoroutineContext 中运行的,所以可以使用 delay 函数。

当然,用 delay(2000) 这种方式保证后台协程能被执行是一种很不“优雅”的做法。可以进行改进:

fun main() = runBlocking {
	val job = GlobalScope.launch { // 1
    delay(1000)
    println("Halloween Special")
  }
  println("Happy")
 	job.join() // 2 
  println("Halloween")
}
  1. 用一个 Job 对象保存创建的协程。
  2. join 函数阻塞父协程(runBlocking),直至 job 执行完。从此也可以看出,job.join() 所处的语句块实际上是在一个协程中,而非主函数体。

最终达到了之前的效果。

再来看一个例子:

fun main() = runBlocking {
	launch { // 1
		delay(1000)
		println("Halloween Special")
	}
	println("Happy")
}

输出:

Happy
Halloween Special

这个例子和上面的唯一区别在于这里用launch 代替了 GlobalScope.launch ,这将使用当前 CoroutineScope(runBlocking)而非新的 CoroutineScope (GlobalScope)来launch 新协程。这种 lanuch 会自动将新协程作为当前 CoroutineScope 的子协程启动,因此不用手动 join。同时,由于当前协程(父协程)是一个 runBlocking 协程,会以阻塞方式运行当前协程,因此 Halloween Special 一句有机会打印。

原因在于:

  1. 协程构建器(比如 runBlocking)会传递一个 CoroutineScope 实例到其作用域。默认会使用这个 CoroutineScope 来 launch 子协程。
  2. 构建器会将作用域(代码块)中所有协程都自动 join(不需要手动 join)到当前协程,由于当前协程是一个 runBlocking 协程,它会阻塞,等所有子协程全部完成才会完成。从这里可以看出,协程的启动或执行方式会受当前 CoroutineScope 的影响。

runBlocking() 除了会等待自身子协程完成任务之外,还会阻塞当前线程。

coroutineScope()

类似 runBlocking,会等待所有子协程完成,但挂起但不阻塞当前线程(挂起不等于阻塞)。

fun main() = runBlocking {
	launch {
		delay(1000)
		println("runBlocking launch") // 2
	}
  println("runBloking print") // 1
  coroutingScope {
    launch {
      delay(5000)
      println("coroutineScope launch") // 4 
    }
    delay(2000)
    println("coroutineScope print") // 3
  }
  println("runBlocking print again") // 5
}

输出结果:

runBlocking print
runBlocking launch
coroutineScope print
coroutineScope launch
runBlocking print again
  1. 先打印 runBlocking print,它前面虽然有一个打印语句,但那是 异步协程 的而且延迟 1 秒后才打印
  2. 1 秒后,打印 lanuch 的协程
  3. coroutineScope 会阻塞当前协程,所以 2 秒后打印 coroutineScope print
  4. coroutineScope 还调动了一个 launch 协程,但它要等待5秒,所以 5 秒后执行这句打印。这个 launch 虽然是一个后台协程,但由于它是 coroutineScope 的子协程,所以 coroutineScope 会等待它的代码执行完才会释放 cpu 执行权。
  5. 这句要等 coroutineScope 执行完才能执行,所以最后打印此句。

这里难于理解的在于 5 处。因为 coroutinScope 明明是不阻塞线程的,但却起到了阻塞的效果。为什么呢?因为 coroutineScope 是一个挂起函数(参考 Suspend 小节),如果其中的协程挂起,那么 coroutineScope 函数也会挂起。换句话说,挂起函数被调用后不会立即返回,而是要等所有挂起函数返回之后才会返回。这样,位于它后面的语句才会被继续执行。在 coroutineScope 挂起期间,执行权交回到外层作用域(调用的函数),但此时当前线程并没有被阻塞,而是在执行其它子协程,比如在 2 处所 launch 的协程。因此 1 秒之后会打印 “runBlocking launch”。如果线程已经阻塞的话,显然 2 是不可能执行的。如果其它协程都执行完了,coroutineScope 仍然还在挂起,那么当前线程会空等待(空事件循环),同时也不会执行下一句代码,直到 coroutineScope 返回。

async()

async() 与 launch() 一样,都会开启单独的非阻塞的后台协程,可以与其它协程并行工作。区别是 async() 的返回值是一个 Deferred 而非 Job。Deferred 和 Job 的区别在于,Deferred 可以拿到代码块的返回值(通过 await() 方法),而 Job 不能。

先来看一个例子:

private suspend fun intValue1():Int {
  delay(2000)
  return 300
}
private suspend fun intValue2():Int {
  delay(3000)
  return 50
}
fun main()=runBlocking{
  val elapsedTime = measureTimemillis{// 执行给定代码块,返回耗时(毫秒)
    val value1 = intValue1()
    val value2 = intValue2()
    println("$value1+$value2=${value1+value2}")
  }
  println("total time: $elapsedTime")
}

打印:

300+50=350
total time: 5017

这两个挂起函数是以串行方式进行。因此执行时间是两个函数的耗时总和。但是实际上它们之间没有依赖关系,是可以以并行方式执行的。因此可以修改为:

fun main()=runBlocking{
   val elapsedTime = measureTimemillis{
      val deferred1 = async { intValue1() } // 启动异步job
  		val deferred2 = async { intValue2() } // 启动异步job
  
  		val value1 = deferred1.await() // 接收异步结果
  		val value2 = deferred2.await() // 接收异步结果
    println("$value1+$value2=${value1+value2}")
  }
  println("total time: $elapsedTime")
}

async() 函数不能立即返回结果,而是分两步走:

  • 首先启动一个异步 job,即 deferred
  • 在 deferred 上调用 await,获得结果

输出结果如下:

300+50=350
total time: 3028

可以看到虽然步骤要多出一步,但由于 async() 是异步执行,程序运行的速度更快了( launch() 也是异步执行,但是没法拿到挂起函数的返回值 )。

async() 的第二个参数是一个 CoroutineStart 枚举,默认为 DEFAULT,表示该job创建后立即根据其上下文调度执行。如果修改为 LAZY,则不会立即执行,而是要等 await() 或start()被调用。

注意,在 LAZY 的情况下,虽然 await() 和 start() 都能启动job程,但二者的结果是不一样的。用 start() 启动的job是异步的,而用 await() 启动的job是同步的。也就是说,如果上述代码如果修改成:

			val deferred1 = async(start=CoroutineStart.LAZY) { intValue1() } 
  		val deferred2 = async(start=CoroutineStart.LAZY) { intValue2() } 

这种情况下,两个job将以同步的方式执行,也就是说,虽然 async 定义了一个 job,但是因为 LAZY 方式导致job没有启动,而是延迟到 await() 的时候才启动,而await() 启动的方式却是同步的,导致程序执行结果是这样的:

300+50=350
total time: 11018

总耗时增加到了11秒(6+2+3)。

解决办法就是在 await 之前先一步用 start() 启动协程:

deferred1.start()
deferred2.start()
val value1 = deferred1.await() 
val value2 = deferred2.await() 

start()会以异步方式启动 job,await() 发现 job 已经启动就不会再去启动。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐