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