协程启动方式及区别

109 阅读5分钟

一、launch

launch()中的block()是挂起函数,不会阻塞当前线程。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {}

1.1、使用样例

GlobalScope.launch (Dispatchers.Main){
    delay(1000)
    println("1协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
}
println("主线程=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

日志输出

主线程=main, id=2
1协程执行=main, id=2

结论:lauch 是非阻塞的

1.2、多个launch情况

GlobalScope.launch (Dispatchers.Main){
    delay(1000)
    println("1-1协程执行—${Thread.currentThread().name}_id_${Thread.currentThread().id}")

    GlobalScope.launch (Dispatchers.Main){
        delay(2000)
        println("2协程执行—${Thread.currentThread().name}_id_${Thread.currentThread().id}")
    }

    GlobalScope.launch (Dispatchers.Main){
        delay(1000)
        println("3协程执行—${Thread.currentThread().name}_id_${Thread.currentThread().id}")
    }
    println("1-2协程执行—${Thread.currentThread().name}_id_${Thread.currentThread().id}")
}
println("主线程—${Thread.currentThread().name}_id_${Thread.currentThread().id}")

日志输出

主线程=main, id=2
1-1协程执行=main, id=2
1-2协程执行=main, id=2
3协程执行=main, id=2
2协程执行=main, id=2

结论:多个launch()是并行执行

1.3、join阻塞

如果需要等待 launch 开启的协程执行完毕,可以使用 join() 方法。join() 会阻塞当前线程,等待协程执行完毕后才释放。

GlobalScope.launch (Dispatchers.Main){
    delay(1000)
    println("1-1协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

    GlobalScope.launch (Dispatchers.Main){
        delay(2000)
        println("2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
    }.join()

    GlobalScope.launch (Dispatchers.Main){
        delay(1000)
        println("3协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
    }.join()
    println("1-2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
}
println("主线程=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

日志输出

主线程=main, id=2
1-1协程执行=main, id=2
2协程执行=main, id=2
3协程执行=main, id=2
1-2协程执行=main, id=2

注意:join必须在suspend函数中或者协程作用域中使用。

二、runBlocking

runBlocking是常规函数,会把当前主线程包装成一个主协程,其会阻塞当前线程,只有当等待其主协程体以及里面的所有子协程执行结束以后,才会让当前线程执行。

public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {}

2.1、使用样例

runBlocking {
    delay(1000)
    println("1-1协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
}
println("主线程=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

日志输出

1-1协程执行=main, id=2
主线程=main, id=2

结论:runBlocking 是阻塞的,一般不使用该函数

三、withContext

withContext()中的block()是挂起函数

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {}

3.1、使用样例

GlobalScope.launch(Dispatchers.Main) {
    delay(1000)
    println("1-1协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

    withContext(Dispatchers.Main) {
        delay(2000)
        println("2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
    }

    withContext(Dispatchers.Main) {
        delay(1000)
        println("3协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
    }
    println("1-2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
}
println("主线程=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

日志输出

主线程=main, id=2
1-1协程执行=main, id=2
2协程执行=main, id=2
3协程执行=main, id=2
1-2协程执行=main, id=2

结论:withContext的代码块是串行执行,同时通过线程池技术执行的任务在不同的线程中。和多个launch()+join()效果一致

四、async + await

4.1、使用样例

var job = GlobalScope.launch (Dispatchers.Main){

    val result1 = async(Dispatchers.IO) {
        delay(2000)
        Log.d("test", "1协程执行—${Thread.currentThread().name}_id_${Thread.currentThread().id}")
    }

    val result2 = async(Dispatchers.IO) {
        delay(1000)
        Log.d("test", "2协程执行— ${Thread.currentThread().name}_id_${Thread.currentThread().id}")
    }

    Log.d("test","result ${result1.await()} ${result2.await()}")

}

日志输出

 2协程执行— DefaultDispatcher-worker-1_id_865
 1协程执行—DefaultDispatcher-worker-5_id_869
 result 1 1

结论:async的代码块是并行执行,同时通过线程池技术执行的任务在不同的线程中

注意:

  • 如果想要多个协程并发执行,并且获得多个返回值,可以先调用 async,最后统一调用 await(),这样能让协程并发执行。(await 会阻塞协程,所以这种写法会导致两个协程串行执行)
  • 不要在每个协程 async 开启后,马上调用 await(),因为 await() 会阻塞当前线程。这样开启的多个协程会串行执行,效率较低。(统一调用 await 方法,让两个协程并行执行,效率高)

五、coroutineScope{ }

也可以CoroutineScope()

5.1、使用样例

GlobalScope.launch (Dispatchers.Main){
    delay(1000)
    println("1-1协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

    coroutineScope{
    delay(2000)
    println("2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
    }

    coroutineScope{
    delay(1000)
    println("3协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
    }
    println("1-2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
}
println("主线程=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

日志输出

主线程=main, id=2
1-1协程执行=main, id=2
2协程执行=main, id=2
3协程执行=main, id=2
1-2协程执行=main, id=2

结论:coroutineScope{}的代码块是串行执行。和多个withContext()效果一致;区别在于withContext()可以切换线程,而coroutineScope{}不行。

六、supervisorScope{ }

6.1、使用样例

GlobalScope.launch (Dispatchers.Main){
    delay(1000)
    println("1-1协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

    supervisorScope{
        delay(2000)
        println("2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
    }

    supervisorScope{
        delay(1000)
        println("3协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
    }
    println("1-2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
}
println("主线程=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

日志输出

主线程=main, id=2
1-1协程执行=main, id=2
2协程执行=main, id=2
3协程执行=main, id=2
1-2协程执行=main, id=2

结论:supervisorScope{}的代码块是串行执行。和多个coroutineScope{}效果一致;区别在于coroutineScope{}是一个协程执行失败了,所有其他兄弟协程执行就会中断 (一毁俱毁),而supervisorScope{}是一个协程执行失败了,不会影响其他兄弟协程的执行 (一毁不俱毁)。

七、suspendCoroutine<?>{ }

7.1、使用样例

GlobalScope.launch (Dispatchers.Main){
    delay(1000)
    println("1-1协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

    suspendCoroutine<Int>{
        println("2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
        it.resume(1)
    }

    suspendCoroutine<Int>{
        println("3协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
    }
    println("1-2协程执行=${Thread.currentThread().name}, id=${Thread.currentThread().id}")
}
println("主线程=${Thread.currentThread().name}, id=${Thread.currentThread().id}")

日志输出

主线程=main, id=2
1-1协程执行=main, id=2
2协程执行=main, id=2
3协程执行=main, id=2
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        //注意:                                          
        //后续没有调用 resume() 执行协程,所以创建完之后需要手动调用 resume()
        safe.getOrThrow()
    }
}

结论:

  • 因为suspendCoroutine()中的block()不是suspend挂起的,所以函数体内无法使用delay()之类的函数;
  • 如果没有主动调用resume(),则协程无法往下回调执行;
  • kotlin协程库delay的具体实现,就是用suspendCoroutine实现的。

八、总结

  • lauch 是非阻塞的。
  • runBlocking 是阻塞的**。**
  • 多个 withContext 任务是串行的, 且withContext 可直接返回耗时任务的结果。
  • 多个 async 任务是并行的,async 返回的是一个Deferred,需要调用其await()方法获取结果。
启动方式运行在任何处运行在suspend函数中运行在CoroutineScope作用域里
launch{ }
runBlocking{ }
withContext{ }
async{ }
coroutineScope{ }
suspendCoroutine<?> { }
supervisorScope{ }