背景
我想在一个 suspend fun 里面 launch 一个协程,应该怎么做?
我用了GlobalScope.launch {}
,但是 IDE 给我标黄了,不建议我这样写,那应该怎么写呢?
带着这个问题,我搜索并阅读了相关参考中的解答,记录为本文。
解答
CoroutineScope
首先是一个接口,这个接口要求有一个CoroutineContext
属性,相当于CoroutineScope
给这个属性包了一层。具体来说,CoroutineScope
是这样定义的:
public interface CoroutineScope { /** * The context of this scope. * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope. * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages. * * By convention, should contain an instance of a [job][Job] to enforce structured concurrency. */ public val coroutineContext: CoroutineContext }
然后,CoroutineScope
同样的名字,还是一个函数,我们代码中调用的就是这个函数。这个CoroutineScope
函数会创建一个给定context
的CoroutineScope
。通过下面的代码我们还可以发现,该函数返回的 scope 的context
里面肯定会带有一个Job
,如果传入的context
没有Job
,函数会给你附送一个。
/** * Creates a [CoroutineScope] that wraps the given coroutine [context]. * * If the given [context] does not contain a [Job] element, then a default `Job()` is created. * This way, cancellation or failure of any child coroutine in this scope cancels all the other children, * just like inside [coroutineScope] block. */ @Suppress("FunctionName") public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job() ) internal class ContextScope(context: CoroutineContext) : CoroutineScope { override val coroutineContext: CoroutineContext = context // CoroutineScope is used intentionally for user-friendly representation override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)" }
上面提到的context
,它包括了一系列的参数,用来决定该协程将如何执行。这些参数主要有:
- CoroutineDispatcher — 分配到哪个线程
- Job — 控制协程的生命周期
- CoroutineName — 协程的名称
- CoroutineExceptionHandler — 处理未被捕获的异常
最重要的两个参数当然是CoroutineDispatcher
和Job
。
Dispatcher 主要有自带的Dispatchers.Default
、Dispatchers.IO
、Dispatchers.Main
(Android)。
CPU密集型的任务用Dispatchers.Default
;而网络、文件的 IO 就用Dispatchers.IO
,大家都懂的。详情可看官方文档。
而 Job 则代表了创建出来的协程,launch()
或aysnc()
会返回一个Job
的实例。可以调用Job
实例的isActive
、isCancelled
获取协程的状态,也可以调用它的cancel()
等方法手动取消这个协程。
而GlobalScope
是什么呢?可以看到,它继承了CoroutineScope
,并且它的context
是固定的EmptyCoroutineContext
。
@DelicateCoroutinesApi public object GlobalScope : CoroutineScope { /** * Returns [EmptyCoroutineContext]. */ override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext }
那么回到最开始的问题,GlobalScope.launch
与CoroutineScope.launch
的区别是什么?
GlobalScope.launch {}
会在顶层创建一个协程,跑在 Dispatchers.Default 所指定的线程中;GlobalScope.launch(Dispatchers.IO) {}
会在顶层创建一个协程,跑在 Dispatchers.IO 对应的 IO 线程中;CoroutineScope(Dispatchers.IO).launch {}
和第 2 个是一样的,也是在顶层创建,只是语法的区别;launch {}
会沿用当前的 context,不在顶层,但本文的背景为“在一个 suspend fun 中创建协程”,在没有 scope 的情况下,是不能直接用launch {}
的;CoroutineScope(currentCoroutineContext()).launch {}
沿用当前的 context,且不在顶层;我在 GitHub 上搜了一下,挺少人这样写的
上面提到的“在顶层”的意思是,不受 structured concurrency 的影响,即不会被父协程的 cancel() 取消,也不会被其他协程抛出的异常导致自己也被退出。
对于一个在顶层被创建的协程,不用的时候记得 cancel(),否则它会在后台一直跑,直到里面的程序结束,这也是 IDE 把 GlobalScope 标黄的原因。
相关参考
测试代码
// 最后附上我测试的代码,test()中有3种写法,在顶层创建的协程不会受RuntimeException的影响 import kotlinx.coroutines.* import kotlin.concurrent.thread fun main() { thread(isDaemon = true) { runBlocking { test() delay(1000L) throw RuntimeException() } } Thread.sleep(5000L) } suspend fun test() { // launch { // 不可以 CoroutineScope(currentCoroutineContext()).launch(Dispatchers.IO) { // CoroutineScope(Dispatchers.Default).launch { // GlobalScope.launch { while (true) { println("${Thread.currentThread().name}, ${currentCoroutineContext()} inside") delay(1000L) } } }