• GlobalScope.launch 与 CoroutineScope.launch 的区别?

    背景

    我想在一个 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函数会创建一个给定contextCoroutineScope。通过下面的代码我们还可以发现,该函数返回的 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 — 处理未被捕获的异常

    最重要的两个参数当然是CoroutineDispatcherJob

    Dispatcher 主要有自带的Dispatchers.DefaultDispatchers.IODispatchers.Main(Android)。

    CPU密集型的任务用Dispatchers.Default;而网络、文件的 IO 就用Dispatchers.IO,大家都懂的。详情可看官方文档

    而 Job 则代表了创建出来的协程,launch()aysnc()会返回一个Job的实例。可以调用Job实例的isActiveisCancelled获取协程的状态,也可以调用它的cancel()等方法手动取消这个协程。

    GlobalScope是什么呢?可以看到,它继承了CoroutineScope,并且它的context是固定的EmptyCoroutineContext

    @DelicateCoroutinesApi
    public object GlobalScope : CoroutineScope {
        /**
         * Returns [EmptyCoroutineContext].
         */
        override val coroutineContext: CoroutineContext
            get() = EmptyCoroutineContext
    }

    那么回到最开始的问题,GlobalScope.launchCoroutineScope.launch的区别是什么?

    1. GlobalScope.launch {}会在顶层创建一个协程,跑在 Dispatchers.Default 所指定的线程中;
    2. GlobalScope.launch(Dispatchers.IO) {}会在顶层创建一个协程,跑在 Dispatchers.IO 对应的 IO 线程中;
    3. CoroutineScope(Dispatchers.IO).launch {}和第 2 个是一样的,也是在顶层创建,只是语法的区别;
    4. launch {}会沿用当前的 context,不在顶层,但本文的背景为“在一个 suspend fun 中创建协程”,在没有 scope 的情况下,是不能直接用launch {}的;
    5. 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)
            }
        }
    }