本系列专栏 #Kotlin协程
前言
前面文章介绍了协程的非阻塞特性和结构化并发特性,在启动协程的函数定义中我们都见到了一个非常熟悉的类:CoroutineContext,这个类可以说是贯穿了整个协程框架,理解CoroutineContext的使用以及常用的类和CoroutineContext的关系对后面理解协程原理非常重要。
正文
CoroutineContext直接翻译就是协程上下文,这里我们先来看个简单的问题:什么是Context。
其实这是一个很有意思的问题,在我们平时编程中很常见各种Context,而翻译为上下文我一直觉得有点问题。假如你在读一篇小说,我突然给你中间一章内容,问你说为什么主角要这样干,你肯定不理解,因为前后章节我都没有看过,即不知道上下文,我也就无法解答这个问题。
这是文章中上下文的意思我们很容易理解,但是程序中的上下文该如何理解呢 我觉得把Context翻译为环境更为合适,即代码执行到代码段A中,这个A需要哪些信息才能正确执行下去,这些信息就保存在Context中,因为我们通常只需要上文就可以了,下文一般不需要。
所以通俗来理解,Context就可以看成是一个容器,程序需要的一些信息没地方保存就可以保存在Context中。或者更为直接的是,你在编码过程中发现你这个代码需要一大堆配置信息,你大可把这些信息都保存在Context中。比如整个Android应用都要用到某个变量,我就把它定义在ApplicationContext中,这个变量只在协程框架中用到,我就定义在CoroutineContext中。
是不是这样理解完就通透了,context就是一堆变量的集和。
指定协程运行的线程池
这里先不说CoroutineContext的设计,先来看一个ConroutineContext子类的使用,即Dispatchers来指定协程运行的线程。
比如下面代码,我想指定协程运行的线程池:
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"
}
比如上面的代码,在main()函数中调用挂起函数,但是在挂起函数的执行过程中,可以使用withContext方法来指定运行的线程,withContext函数如下:
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
}
可以发现这里Dispatchers.IO就是一个CoroutineContext,好我们来运行一下代码:
可以发现在切换IO线程前,协程是运行在main线程上,当切换了IO线程后,协程运行在worker-1线程上,然后在执行完后,又切换了回来。
不止这个withContext,包括runBlocking,launch的第一个参数都是CoroutineContext,我们可以用来指定运行的线程。比如下面代码:
fun main() = runBlocking(Dispatchers.IO) {
val user = getUserInfo()
logX(user)
}
suspend fun getUserInfo(): String {
logX("Before IO Context.")
withContext(Dispatchers.Default) {
logX("In IO Context.")
delay(1000L)
}
logX("After IO Context.")
return "BoyCoder"
}
在runBlocking中指定运行的线程,这里我们指定的IO线程,下面指定的是Default线程,我们来看一下代码运行结果:
会发现有的在worker-1中,有的在worker-3中,但是都是DefaultDispatcher的线程池,按理说应该是IO的工作线程才对啊,这就要明白Dispatchers几个内置的分别是啥意思。
内置的Dispatcher
Kotlin官方提供了几种内置的Dispatcher,分别如下:
- Dispatchers.Main,它只在UI编程平台才有意义,比如Android平台,这种平台只有Main线程才能用于UI绘制。
- Dispatchers.Unconfined,代表无所谓,当前协程可以运行在任意线程上。
- Dispatchers.Default,用于CPU密集型任务的线程池,一般来说它内部的线程个数与机器CPU核心数量保持一致。
- Dispatchers.IO,用于IO密集型任务的线程池,内部线程数量较多,一般为64个。
需要特别注意的是,Dispatchers.IO底层是可能复用Dispatchers.Default中的线程,比如上面截图中的结果,就都是复用Default线程池中的线程。
自定义Dispatcher
这里除了Kotlin官方内置的几个Dispathcer可以选择外,还可以自定义Dispatcher,比如下面代码:
val mySingleDispatcher = Executors.newSingleThreadExecutor {
Thread(it,"MySingleThread").apply { isDaemon = true }
}.asCoroutineDispatcher()
我们创建了一个单线程的线程池,然后通过asCoroutineDispathcer方法转换为Dispatcher,然后我们来进行使用如下:
fun main() = runBlocking(mySingleDispatcher) {
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"
}
这里的runBlocking就传入我们自定义的Dispather,然后运行结果如下:
由于只有1个线程的线程池,所以切换IO线程时,会复用Default中的线程池。
这其实也就印证了协程是运行在线程上的Task这种说法。
万物皆为Context
前面我们说了CoroutineDispatcher就是一个CoroutineContext,其实在协程中我们见到的几乎所有重要概念它都是CoroutineContext,比如Job、CoroutineScope等,是不是觉得有点不可思议,我们先来看看这些类和CoroutineContext的关系,后面再说协程为什么这样设计。
CoroutineScope
协程作用域,我们前面文章说了launch和async都是协程作用域CoroutineScope的扩展函数,我们来看一下这个CoroutineScope:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
会发现它就是一个简单的接口,而这个接口的唯一成员就是CoroutineContext,所以CoroutineScope只是对CoroutineContext做了一层简单封装而已,其核心能力还是CoroutineContext。
(涉及后面文章更新感悟,可以暂不理解:这里的Context其实就是协程运行的必要环境信息,而Scope把这个Context封装了一层,然后在launch启动时,就会获取父协程的环境变量信息,从而让子协程和父协程产生关系,这也是结构化并发的原因,在后面CoroutineScope原理解析时会详细说明。)
而协程作用域的最大作用就是可以方便批量控制协程,说道批量控制协程,不由得想起来上篇文章所说的结构化并发,看下面代码:
fun main() = runBlocking {
// 仅用于测试,生成环境不要使用这么简易的CoroutineScope
val scope = CoroutineScope(Job())
scope.launch {
logX("First start!")
delay(1000L)
logX("First end!") // 不会执行
}
scope.launch {
logX("Second start!")
delay(1000L)
logX("Second end!") // 不会执行
}
scope.launch {
logX("Third start!")
delay(1000L)
logX("Third end!") // 不会执行
}
delay(500L)
scope.cancel()
delay(1000L)
}
可以发现这里创建了一个scope,然后用这个scope启动了3个协程,当协程没有执行完成时,通过调用scope的cancel方法便可以取消这3个协程,上述代码打印如下:
这同样体现了结构化并发的理念。
Job
前面说了CoroutineScope还是封装的CoroutineContext的话,那Job就是一个真正的CoroutineContext了,我们来看一下源码:
public interface Job : CoroutineContext.Element {}
public interface Element : CoroutineContext {}
可以发现这里通过2层继承,Job就是CoroutineContext的子类。
Dispatcher
前面使用案例中说了内置的Dispatcher也是一个CoroutineContext,我们来看一下是如何关联的,下面是Dispatchers的源码:
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
@JvmStatic
public val IO: CoroutineDispatcher = DefaultIoScheduler
}
会发现这是一个单例,这样很符合我们的使用,而这里提供了几个线程池,比如Default,它的类型是CoroutineDispathcer,我们来看一下:
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor
public interface ContinuationInterceptor : CoroutineContext.Element
根据这个CoroutineDispatcher的继承关系,最终发现还是继承至CoroutineContext。
CoroutineNmae
从名字就可以看出这个是用来给协程命名的,比如下面代码:
fun main() = runBlocking {
val scope = CoroutineScope(Job() + mySingleDispatcher)
scope.launch(CoroutineName("MyCoroutine")) {
logX(coroutineContext[CoroutineDispatcher] == mySingleDispatcher)
delay(1000L)
logX("MyCoroutine end!")
}
delay(500L)
scope.cancel()
delay(1000L)
}
这里我们在启动协程时传入一个名字,那打印如下:
会发现这里在线程名后@的协程名,就是我们命名的,而#2是一个自增的ID。
CoroutineExceptionHandler
这个也是从名字就可以看出其作用,即协程异常处理,我们看一下使用,代码如下:
fun main() = runBlocking {
val scope = CoroutineScope(Job() + mySingleDispatcher)
val myExceptionHandler = CoroutineExceptionHandler{_,thorwable ->
println("Catch exception : $thorwable")
}
scope.launch(CoroutineName("MyCoroutine") + myExceptionHandler) {
logX(coroutineContext[CoroutineDispatcher] == mySingleDispatcher)
val s: String? = null
s!!.length
delay(1000L)
logX("MyCoroutine end!")
}
delay(500L)
scope.cancel()
delay(1000L)
}
这里在scope启动协程时,传递了协程名Context和异常处理Context,通过加号进行连接,然后这个协程的异常就可以被这个异常处理器捕获,打印如下:
CoroutineContext接口
看了上面我们不免发现协程中居然这么多重要的概念都是CoroutineContext的子类,我们来看看该类的源码:
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
}
这里的接口设计看着非常像是集和,确切的说很像集和中的Map结构,对比如下图:
所以我们完全可以把CoroutineContext当作Map来使用。
这里为什么要这样设计,我个人觉得有以下原因:
1、类似Map结构,可以方便运算符重载,构造Context,比如下面代码
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)
}
这里创建了一个scope,但是创建是使用Job() + mySingleDispatcher这种方式,这里之所以能使用加号,是因为运算符重载,就不多说了,而这种创建的scope就可以指定其CoroutineDispatcher。
根据前面可知scope本身也是CoroutineContext的封装,所以这里的 coroutineContext变量就是scope的成员变量,而又由于运算符重载的问题,coroutineContext[CoroutineDispatcher]就可以取出该协程的调度器。
所以再次印证了,在使用CoroutineContext时,完全可以看成是一个Map。
2、从Context本质出发,在文章开始说了,它其实就是一大堆环境变量的集和,而这些比如Job、CoroutineDispatcher等都可以看成是协程运行的辅助环境变量。
理解这个可以借鉴后面的有篇说Continuation的文章,里面有个协程框架架构,其实CoroutineContext是最底层的概念,而launch、Dispatcher等则是中间层的概念,毫不夸张的说不要这些中间层API,协程一样可以运行,但是就没有这么多特性。
所以说这些中层概念之所以为Context,其实都可以看成是协程运行的辅助环境变量。
总结
本篇文章没有太多深入,只是介绍了一些我们常见的类和CoroutineContext的关系,其实这里涉及到了协程框架的设计,等后面说原理时再讨论。下面做个简单总结:
- CoroutineContext本身是一个接口,而它的接口设计和Map的API极为相似,在使用过程中,可以当成Map来使用。这种设计的优点也就是方便我们使用运算符重载来创建我们希望的各种协程组件。
- 协程中非常多的类,本身就是CoroutineContext,比如Job,Deferred,Dispatcher,ContinuationInterceptor、CoroutineNmae、CoroutineExceptionHandler,也正是由于这些是一个接口的子类,所以可以使用操作符重载来写出灵活的代码 。
- 协程的CoroutineScope是CoroutineContext的一层简单封装。
- 挂起函数也和CoroutineContext有关系,后面再说。
所以可以用下面这个图做个总结: