背景
我想在一个 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)
}
}
}