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)
        }
    }
}

发表评论