协程的 Context,在 Kotlin 当中有一个具体的名字,叫做 CoroutineContext。
从概念上讲,CoroutineContext 很容易理解,它只是个上下文而已,实际开发中它最常见的用处就是切换线程池。
Context 的应用
CoroutineContext 就是协程的上下文。launch 源码里面,CoroutineContext 其实就是函数的第一个参数:
// 代码段1
public fun CoroutineScope.launch(
// 这里
// ↓
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {}
如果想要指定 launch 工作的线程池的话,需要自己传 context 参数。没有传 context 参数,会使用默认值 EmptyCoroutineContext,顾名思义,这就是一个空的上下文对象。
举例:在挂起函数 getUserInfo() 当中,用 withContext() 这个函数,传入“Dispatchers.IO”,这就是 Kotlin 官方提供的一个 CoroutineContext 对象。
// 代码段2
fun main() = runBlocking {
val user = getUserInfo()
logX(user)
}
suspend fun getUserInfo(): String {
logX("Before IO Context.")
withContext(Dispatchers.IO) {
logX("In IO Context.")
delay(1000L)
}
logX("After IO Context.")
return "BoyCoder"
}
/*
输出结果:
================================
Before IO Context.
Thread:main @coroutine#1
================================
================================
In IO Context.
Thread:DefaultDispatcher-worker-1 @coroutine#1
================================
================================
After IO Context.
Thread:main @coroutine#1
================================
================================
BoyCoder
Thread:main @coroutine#1
================================
*/
在 withContext() 这里指定线程池以后,Lambda 当中的代码就会被分发到 DefaultDispatcher 线程池中去执行,而它外部的所有代码仍然还是运行在 main 之上。
runBlocking 这个函数的第一个参数也是 CoroutineContext,所以,我们也可以传入一个 Dispatcher 对象作为参数:
// 代码段4
// 变化在这里
// ↓
fun main() = runBlocking(Dispatchers.IO) {
val user = getUserInfo()
logX(user)
}
所有的代码都运行在 DefaultDispatcher 这个线程池当中了。而 Kotlin 官方除了提供了 Dispatchers.IO 以外,还提供了 Dispatchers.Main、Dispatchers.Unconfined、Dispatchers.Default 这几种内置 Dispatcher。
需要特别注意的是,Dispatchers.IO 底层是可能复用 Dispatchers.Default 当中的线程的。当 Dispatchers.Default 线程池当中有富余线程的时候,它是可以被 IO 线程池复用的。
如果指定了 Dispatchers.Unconfined 这个特殊的 Dispatcher,会改变代码执行顺序。 其实 Unconfined 代表的意思就是,当前协程可能运行在任何线程之上,不作强制要求。由此可见,Dispatchers.Unconfined 其实是很危险的。所以,我们不应该随意使用 Dispatchers.Unconfined。
万物皆有 Context
在 Kotlin 协程当中,但凡是重要的概念,都或多或少跟 CoroutineContext 有关系:Job、Dispatcher、CoroutineExceptionHandler、CoroutineScope,甚至挂起函数,它们都跟 CoroutineContext 有着密切的联系。甚至,它们之中的 Job、Dispatcher、CoroutineExceptionHandler 本身,就是 Context。
CoroutineScope
如果要调用 launch,就必须先有“协程作用域”,也就是 CoroutineScope。
// 代码段9
// 注意这里
// ↓
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {}
// CoroutineScope 源码
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
CoroutineScope 的源码,其实就是一个简单的接口,而这个接口只有唯一的成员,就是 CoroutineContext。所以,CoroutineScope 只是对 CoroutineContext 做了一层封装而已,它的核心能力其实都来自于 CoroutineContext。而 CoroutineScope 最大的作用,就是可以方便我们批量控制协程。能体现协程结构化并发的理念。
Job 和 Dispatcher
如果说 CoroutineScope 是封装了 CoroutineContext,那么 Job 就是一个真正的 CoroutineContext 了。
// 代码段11
public interface Job : CoroutineContext.Element {}
public interface CoroutineContext {
public interface Element : CoroutineContext {}
}
Job 继承自 CoroutineContext.Element,而 CoroutineContext.Element 仍然继承自 CoroutineContext,这就意味着 Job 是间接继承自 CoroutineContext 的。所以说,Job 确实是一个真正的 CoroutineContext。
所以,写这样的代码也完全没问题:
// 代码段12
fun main() = runBlocking {
val job: CoroutineContext = Job()
}
CoroutineContext 本身的接口设计:
// 代码段13
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public operator fun plus(context: CoroutineContext): CoroutineContext {}
public fun minusKey(key: Key<*>): CoroutineContext
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public interface Key<E : Element>
}
从上面代码中的 get()、plus()、minusKey()、fold() 这几个方法,我们可以看到 CoroutineContext 的接口设计,就跟集合 API 一样。准确来说,它的 API 设计和 Map 十分类似。
所以,我们完全可以把 CoroutineContext 当作 Map 来用。
// 代码段14
//mySingleDispatcher是自定义的Dispatcher
@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking {
// 注意这里
val scope = CoroutineScope(Job() + mySingleDispatcher)
scope.launch {
// 注意这里
logX(coroutineContext[CoroutineDispatcher] == mySingleDispatcher)
delay(1000L)
logX("First end!") // 不会执行
}
delay(500L)
scope.cancel()
delay(1000L)
}
/*
输出结果:
================================
true
Thread:MySingleThread @coroutine#2
================================
*/
上面的代码使用了“Job() + mySingleDispatcher”这样的方式创建 CoroutineScope,代码之所以这么写,是因为 CoroutineContext 的 plus() 进行了操作符重载。
// 代码段15
// 操作符重载
// ↓
public operator fun <E : Element> plus(key: Key<E>): E?
注意这里代码中的 operator 关键字,如果少了它,我们就得换一种方式了:mySingleDispatcher.plus(Job())。因为,当我们用 operator 修饰 plus() 方法以后,就可以用“+”来重载这个方法,类似的,List 和 Map 都支持这样的写法:list3 = list1+list2、map3 = map1 + map2,这代表集合之间的合并。
另外,我们还使用了“coroutineContext[CoroutineDispatcher]”这样的方式,访问当前协程所对应的 Dispatcher。这也是因为 CoroutineContext 的 get(),支持了操作符重载。
// 代码段16
// 操作符重载
// ↓
public operator fun <E : Element> get(key: Key<E>): E?
实际上,在 Kotlin 当中很多集合也是支持 get() 方法重载的,比如 List、Map,我们都可以使用这样的语法:list[0]、map[key],以数组下标的方式来访问集合元素。
Kotlin 官方的源代码当中大量使用了操作符重载来简化代码逻辑,而 CoroutineContext 就是一个最典型的例子。
Dispatcher 本身也是 CoroutineContext,不然它怎么可以实现“Job() + mySingleDispatcher”这样的写法呢?最重要的是,当我们以这样的方式创建出 scope 以后,后续创建的协程就全部都运行在 mySingleDispatcher 这个线程之上了。
那么,Dispatcher 到底是如何跟 CoroutineContext 建立关系的呢?看看它的源码:
// 代码段17
public actual object Dispatchers {
public actual val Default: CoroutineDispatcher = DefaultScheduler
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
public val IO: CoroutineDispatcher = DefaultIoScheduler
public fun shutdown() { }
}
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {}
public interface ContinuationInterceptor : CoroutineContext.Element {}
可以看到,Dispatchers 其实是一个 object 单例,它的内部成员的类型是 CoroutineDispatcher,而它又是继承自 ContinuationInterceptor,这个类则是实现了 CoroutineContext.Element 接口。由此可见,Dispatcher 确实就是 CoroutineContext。
其他 CoroutineContext
CoroutineName,当我们创建协程的时候,可以传入指定的名称。比如:
// 代码段18
@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking {
val scope = CoroutineScope(Job() + mySingleDispatcher)
// 注意这里
scope.launch(CoroutineName("MyFirstCoroutine!")) {
logX(coroutineContext[CoroutineDispatcher] == mySingleDispatcher)
delay(1000L)
logX("First end!")
}
delay(500L)
scope.cancel()
delay(1000L)
}
/*
输出结果:
================================
true
Thread:MySingleThread @MyFirstCoroutine!#2 // 注意这里
================================
*/
调用 launch 传入“CoroutineName(“MyFirstCoroutine!”)”作为协程的名字。在后面输出的结果中,得到“@MyFirstCoroutine!#2”这样的输出。由此可见,其中的数字“2”,其实是一个自增的唯一 ID。
CoroutineExceptionHandler,它主要负责处理协程当中的异常。
// 代码段19
public interface CoroutineExceptionHandler : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
public fun handleException(context: CoroutineContext, exception: Throwable)
}
CoroutineExceptionHandler 的接口定义其实很简单,我们基本上一眼就能看懂。CoroutineExceptionHandler 真正重要的,其实只有 handleException() 这个方法,如果我们要自定义异常处理器,我们就只需要实现该方法即可。
// 代码段20
// 这里使用了挂起函数版本的main()
suspend fun main() {
val myExceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("Catch exception: $throwable")
}
val scope = CoroutineScope(Job() + mySingleDispatcher)
val job = scope.launch(myExceptionHandler) {
val s: String? = null
s!!.length // 空指针异常
}
job.join()
}
/*
输出结果:
Catch exception: java.lang.NullPointerException
*/
小结:
CoroutineContext,是 Kotlin 协程当中非常关键的一个概念。它本身是一个接口,但它的接口设计与 Map 的 API 极为相似,我们在使用的过程中,也可以把它当作 Map 来用。
协程里很多重要的类,它们本身都是 CoroutineContext。比如 Job、Deferred、Dispatcher、ContinuationInterceptor、CoroutineName、CoroutineExceptionHandler,它们都继承自 CoroutineContext 这个接口。也正因为它们都继承了 CoroutineContext 接口,所以我们可以通过操作符重载的方式,写出更加灵活的代码,比如“Job() + mySingleDispatcher+CoroutineName(“MyFirstCoroutine!”)”。
协程当中的 CoroutineScope,本质上也是 CoroutineContext 的一层简单封装。
另外,协程里极其重要的“挂起函数”,它与 CoroutineContext 之间也有着非常紧密的联系。
下面的结构图,描述了 CoroutineContext 元素之间的关系:
总的来说,Job、Dispatcher、CoroutineName,它们本质上只是 CoroutieContext 这个集合当中的一种数据类型,只是恰好 Kotlin 官方让它们都继承了 CoroutineContext 这个接口。而 CoroutineScope 则是对 CoroutineContext 的进一步封装,它的核心能力,全部都是源自于 CoroutineContext。