【深入kotlin】 - 创建协程
创建协程的方式主要有:launch()、async()、coroutineScope()、runBlocking() 。launch()创建一个异步协程(非阻塞),返回一个不带返回值的 job。fun main(){GlobalScope.launch{// 默认 CorutineDispatcher 为 Dispatchers.Defaultdelay(1000) // 仅可用于协程,而非线程p
创建协程的方式主要有: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 秒后打印
- 此句最先打印
- 此句延迟两秒,所以在 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")
}
- 用一个 Job 对象保存创建的协程。
- 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 一句有机会打印。
原因在于:
- 协程构建器(比如 runBlocking)会传递一个 CoroutineScope 实例到其作用域。默认会使用这个 CoroutineScope 来 launch 子协程。
- 构建器会将作用域(代码块)中所有协程都自动 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
- 先打印 runBlocking print,它前面虽然有一个打印语句,但那是 异步协程 的而且延迟 1 秒后才打印
- 1 秒后,打印 lanuch 的协程
- coroutineScope 会阻塞当前协程,所以 2 秒后打印 coroutineScope print
- coroutineScope 还调动了一个 launch 协程,但它要等待5秒,所以 5 秒后执行这句打印。这个 launch 虽然是一个后台协程,但由于它是 coroutineScope 的子协程,所以 coroutineScope 会等待它的代码执行完才会释放 cpu 执行权。
- 这句要等 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 已经启动就不会再去启动。
更多推荐


所有评论(0)