CoroutineScope 与 CoroutineContext 的概念
CoroutineContext (协程上下文)
CoroutineContext
是协程上下文,包含了协程运行时所需的所有信息。
比如:
- 管理协程流程(生命周期)的
Job
。 - 管理线程的
ContinuationInterceptor
,它的实现类CoroutineDispatcher
决定了协程所运行的线程或线程池。
CoroutineScope (协程作用域)
CoroutineScope
是协程作用域,它通过 coroutineContext
属性持有了当前协程代码块的上下文信息。
比如,我们可以获取 Job
和 ContinuationInterceptor
对象:
kotlin
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext) // scope 并没有持有已有协程的上下文
val outerJob = scope.launch {
val innerJob = coroutineContext[Job]
val interceptor = coroutineContext[ContinuationInterceptor]
println("job: $innerJob, interceptor: $interceptor")
}
outerJob.join()
}
CoroutineScope
的另一个作用就是提供了 launch
和 async
协程构建器,我们可以通过它来启动一个协程。
这样,新创建的协程能够自动继承 CoroutineScope
的 coroutineContext
。比如利用 Job
,可以建立起父子关系,从而实现结构化并发。
GlobalScope
GlobalScope
是一个单例的 CoroutineScope
对象,所以我们在任何地方通过它来启动协程。
它的第二个特点是,它的 coroutineContext
属性是 EmptyCoroutineContext
,也就是说它没有内置的 Job
。
kotlin
@DelicateCoroutinesApi
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
即使是我们手动创建的 CoroutineScope
,其内部也是有 Job
的。
kotlin
// 手动创建 CoroutineScope
CoroutineScope(EmptyCoroutineContext)
// CoroutineScope.kt
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job()) // 自动创建Job对象
所以我们在 GlobalScope.coroutineContext
中是获取不到 Job
的:
kotlin
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking<Unit> {
val job: Job? = GlobalScope.coroutineContext[Job]
if (job == null) {
println("job is null")
}
try {
val jobNotNull: Job = GlobalScope.coroutineContext.job
} catch (e: IllegalStateException) {
println("job is null, exception is: $e")
}
}
运行结果:
less
job is null
job is null, exception is: java.lang.IllegalStateException: Current context doesn't contain Job in it: EmptyCoroutineContext
那么,这有什么用吗?
其实,GlobalScope
所启动的协程没有父 Job
。
这就意味着:
- 当前协程不和其他
Job
的生命周期绑定,比如不会随着某个界面的关闭而自动取消。 - 它是顶级协程,生命周期默认为整个应用的生命周期。
- 它发生异常,并不会影响到其他协程和
GlobalScope
。反之,GlobalScope
本身也无法级联取消所有任务,因为它所启动的协程是完全独立的。
总结:GlobalScope
就是用来启动那些不与组件生命周期绑定,而是与整个应用生命周期保持一致的全局任务,比如一个日志上报任务。
关键在使用时,可能会有资源泄露的风险,需要正确管理好协程的生命周期。
Context 的三个实用工具
在挂起函数中获取 CoroutineContext
如果我们要在一个挂起函数中获取 CoroutineContext
,我们不得不给将其作为 CoroutineScope
的扩展函数。
kotlin
suspend fun CoroutineScope.printContinuationInterceptor() {
delay(1000)
val interceptor = coroutineContext[ContinuationInterceptor]
println("interceptor: $interceptor")
}
但我们知道挂起函数的外部一定有协程存在,所以是存在 CoroutineContext
的。为此,Kotlin 协程库提供了一个顶层的 coroutineContext
属性,这个属性的 get()
函数是一个挂起函数,它能在任何挂起函数中访问到当前正在执行的协程的 CoroutineContext
。
kotlin
import kotlin.coroutines.coroutineContext
suspend fun printContinuationInterceptor() {
delay(1000)
val interceptor = coroutineContext[ContinuationInterceptor]
println("interceptor: $interceptor")
}
另外,还有一个 currentCoroutineContext()
函数也能获取到 CoroutineContext
,它内部实现也是 coroutineContext
属性。
为什么需要这个函数?
为了解决命名冲突,比如下面这段代码。
kotlin
private fun mySuspendFun() {
flow<String> {
// 顶层属性
coroutineContext
}
GlobalScope.launch {
flow<String> {
// this 的成员属性优先级高于顶层属性
// 所以是外层 launch 的 CoroutineScope 的成员属性 coroutineContext
coroutineContext
}
}
}
在这种情况下,如果需要明确属性的源头,就需要使用 currentCoroutineContext()
函数,它会调用到那个顶层的属性。
CoroutineName 协程命名
CoroutineName
是一个协程上下文信息,我们可以使用它来给协程设置一个名称。
kotlin
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val name = CoroutineName("coroutine-1")
val job = scope.launch(name) {
val coroutineName = coroutineContext[CoroutineName]
println("current coroutine name: $coroutineName")
}
job.join()
}
运行结果:
less
current coroutine name: CoroutineName(coroutine-1)
它主要用于测试和调试,你可以使用它来区分哪些日志是哪个协程打印的。
自定义 CoroutineContext
如果我们要给协程附加一些功能,我们可以考虑自定义 CoroutineContext
。
如果是简单的标记,可以优先考虑使用
CoroutineName
。
自定义 CoroutineContext
需要实现 CoroutineContext.Element
,并且提供 Key
。为此,Kotlin 协程库提供了 AbstractCoroutineContextElement
来简化这个过程。我们只需这样,即可创建一个用于协程内部记录日志的 Context
:
kotlin
// 继承 AbstractCoroutineContextElement,并把 Key 传给父构造函数
class CoroutineLogger(val tag: String) : AbstractCoroutineContextElement(CoroutineLogger) {
// 声明专属的 Key
companion object Key : CoroutineContext.Key<CoroutineLogger>
// 添加专属功能
fun log(message: String) {
println("[$tag] $message")
}
}
使用示例:
kotlin
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch(CoroutineLogger("Test")) {
val logger = coroutineContext[CoroutineLogger]
logger?.log("Start")
delay(5000)
logger?.log("End")
}
job.join()
}
运行结果:
less
[Test] Start
[Test] End
coroutineScope() 与 withContext()
coroutineScope 串行的异常封装器
coroutineScope
是一个挂起函数,它会挂起当前协程,直到执行完内部的所有代码(包括会等待内部启动的所有子协程执行完毕),最后一行代码的执行结果会作为函数的返回值。
coroutineScope
会创建一个新的 CoroutineScope
,在这个作用域中执行 block
代码块。并且这个作用域严格继承了父上下文(coroutineContext
),并会在内部创建一个新的 Job
,作为父 Job
的子 Job
。
coroutineScope
从效果上来看,和launch().join()
类似。
那么,它的应用场景是什么?
它的应用场景由它的特性决定,有两个核心场景:
-
在挂起函数中提供
CoroutineScope
。(最常用)kotlinsuspend fun CoroutineScope.mySuspendFunction() { delay(1000) launch { println("launch") } }
如果你要在挂起函数中启动一个新的协程,你只好将其定义为
CoroutineScope
的扩展函数。不过,你也可以使用coroutineScope
来提供作用域。它能提供作用域,是因为挂起函数的外部一定存在着协程,所以一定具有
CoroutineScope
。kotlinsuspend fun doConcurrentWork() { val startTime = System.currentTimeMillis() coroutineScope { val task1 = async { // 任务1 delay(5000) } val task2 = async { // 任务2 delay(3000) } } // // 挂起,直到上面两个 async 都完成 val endTime = System.currentTimeMillis() println("Total execution time: ${endTime - startTime}") // 5000 左右 }
-
业务逻辑封装并进行异常处理。(最重要)
我们都知道,我们无法在协程外部使用
try-catch
捕获协程内部的异常。但使用
coroutineScope
函数可以,当它内部的任何子协程失败了,它会将这个异常重新抛出来,这时我们可以使用try-catch
来捕获。kotlinfun main() = runBlocking<Unit> { try { coroutineScope { val data1 = async { "user-1" } val data2 = async { throw IllegalStateException("error") } awaitAll(data1, data2) } } catch (e: Exception) { println("exception is: $e") } }
运行结果:
lessexception is: java.lang.IllegalStateException: error
原因也很简单,因为它是一个串行的挂起函数,外部协程会被挂起,直到它执行完毕。如果它的内部出现了异常,外部协程是能够知晓的。
coroutineScope
可以将并发的崩溃变为可被捕获、处理的异常,常用于处理并发错误。
withContext 串行的上下文切换器
我们再来看 withContext
,其实它和 coroutineScope
几乎一样。
它也是一个串行的挂起函数,也会返回代码块的结果,内部也是启动了一个新的协程。
它和 coroutineScope
的唯一的不同是,withContext
允许我们传递上下文。你也可以这么想,coroutineScope
就是一个不改变任何上下文的 withContext
:
kotlin
withContext(EmptyCoroutineContext) { // 沿用旧的 CoroutineContext
}
withContext(coroutineContext) { // 使用旧的 CoroutineContext
}
而 withContext
的使用场景就很清楚了,我们需要切换上下文的时候会使用它,并且希望代码是串行执行的,之后还能再切回原来的线程继续往下执行。
虽然
withContext
与coroutineScope
类似,但coroutineScope
更多用于封装业务异常。
kotlin
suspend fun getUserProfile() {
// 当前在 Dispatchers.Main
val profile = withContext(Dispatchers.IO) {
// 自动切换到 IO 线程
Thread.sleep(3000) // 耗时操作
"the user profile"
}
// 自动切回 Dispatchers.Main
println("the user profile is $profile")
}
CoroutineContext 的加、取操作
加法:合并与替换
两个 CoroutineContext
相加调用的是 plus()
。
kotlin
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
其中关键在于 CombinedContext
,它是 CoroutineContext
的实现类:
kotlin
// CoroutineContextImpl.kt
@SinceKotlin("1.3")
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element // Element 也是 `CoroutineContext` 的实现类
) : CoroutineContext, Serializable
它会将操作符两边的上下文使用 CombinedContext
对象包裹(合并),如果两个上下文具有相同的 Key
,加号右侧的会替换左侧的。
比如 Dispatchers.IO + Job() + CoroutineName("my-name")
一共会进行三次合并,得到三个 CombinedContext
对象,不会进行替换。
kotlin
fun main() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, throwable ->
println("Caught $throwable")
}
val job =
launch(Dispatchers.IO + Job() + CoroutineName("my-name") + handler) {
println(coroutineContext)
}
job.join()
}
运行结果:
less
[CoroutineName(my-name), com.example.coroutinetest.TestKtKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@6667fe3b, StandaloneCoroutine{Active}@26b2b4fd, Dispatchers.IO]
如果在末尾再加上一个 CoroutineName("your_name")
,会进行一次替换,运行结果是:[com.example.coroutinetest.TestKtKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@6667fe3b, CoroutineName(your_name), StandaloneCoroutine{Active}@26b2b4fd, Dispatchers.IO]
[] 取值
[]
取值其实调用的是 CoroutineContext.get()
函数,它会从上下文(CombinedContext
树)中找到我们需要的信息。
kotlin
@SinceKotlin("1.3")
public interface CoroutineContext {
/**
* Returns the element with the given [key] from this context or `null`.
*/
public operator fun <E : Element> get(key: Key<E>): E?
// ...
}
我们填入的参数其实是每一个接口的伴生对象 Key
,每个伴生对象都实现了 CoroutineContext.Key<T>
接口,并将泛型指定为了当前接口。
以 ContinuationInterceptor
为例:
kotlin
@SinceKotlin("1.3")
public interface ContinuationInterceptor : CoroutineContext.Element {
/**
* The key that defines *the* context interceptor.
*/
public companion object Key : CoroutineContext.Key<ContinuationInterceptor>
// ...
}
比如我们要获取上下文中的 CoroutineDispatcher
,我们可以这样做:
kotlin
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
val dispatcher = coroutineContext[ContinuationInterceptor] as CoroutineDispatcher // 强转
println("CoroutineDispatcher is $dispatcher")
}
job.join()
}