Kotlin 朱涛-17 协程 上下文 CoroutineContext 线程池
目录
17 | Context:万物皆有 Context
从概念上讲,CoroutineContext 只是个上下文
而已,开发中最常见的用处就是切换线程池
,但其背后的代码设计其实比较复杂,Kotlin 协程中比较重要的概念,都或多或少跟 CoroutineContext 有关系。
CoroutineContext 简介
前面我们在很多地方已经见过 CoroutineContext:
launch()
、async()
函数的第一个参数就是CoroutineContext
,默认值是EmptyCoroutineContext
runBlocking()
函数的第一个参数也是CoroutineContext
,默认值是 an internal implementation of event loop,这个值可以理解为是,在运行时 Kotlin 编译器自动帮我们插入的withContext()
函数的第一个参数也是CoroutineContext
,他没有默认值
注意:CoroutineContext 定义在 Kotlin 标准库而非扩展库中,导包时注意包名为
kotlin.coroutines
。
接口设计
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>
}
CoroutineContext 的 API 设计和 Map 十分类似:
操作符重载案例
@ExperimentalStdlibApi
fun main() = runBlocking {
val job = Job()
val dispatcher: ExecutorCoroutineDispatcher = Executors.newSingleThreadExecutor {
Thread(it, "MySingleThread").apply { isDaemon = true }
}.asCoroutineDispatcher()
val scope = CoroutineScope(job + dispatcher) // 操作符重载
scope.launch {
log(scope.coroutineContext[Job] === job) // 操作符重载
log(scope.coroutineContext[CoroutineDispatcher] === dispatcher)
log(scope.coroutineContext.get(CoroutineDispatcher) === dispatcher)
log(coroutineContext[ExecutorCoroutineDispatcher] === dispatcher)
}
delay(500L)
}
fun log(text: Any) = println("$text - ${Thread.currentThread().name}".trimIndent())
以上打印结果均为:true - MySingleThread @coroutine#2
job + dispatcher
的意义是:同时指定 parentJob 和 线程池
操作符重载详解
在上面的代码中,我们:
- 使用了
job + dispatcher
这样的方式,创建CoroutineScope
- 使用了
coroutineContext[XX]
这样的方式,访问当前协程所对应的XX
代码之所以这么写,是因为 CoroutineContext 的 plus/get
方法支持操作符重载:
public operator fun plus(context: CoroutineContext): CoroutineContext
public operator fun <E : Element> get(key: Key<E>): E?
- 用
operator
修饰plus()
方法后,就可以用+
来重载这个方法- 比如,集合之间的合并操作:
list3 = list1 + list2
、map3 = map1 + map2
- 比如,集合之间的合并操作:
- 用
operator
修饰get()
方法后,就可以用[]
来重载这个方法- 比如,以数组下标的方式访问集合的元素:
list[0]
、map[key]
- 比如,以数组下标的方式访问集合的元素:
如果 plus/get
方法声明中去掉了关键字 operator
,就只能使用下面的方式了:
job.plus(dispatcher) // 或 dispatcher.plus(job)
scope.coroutineContext.get(CoroutineDispatcher)
Kotlin 中的集合与数组
的访问方式,之所以可以保持一致,就是依赖于操作符重载
。实际上,Kotlin 官方的源代码当中大量使用了操作符重载来简化代码逻辑,而 CoroutineContext 就是一个最典型的例子。
挂起函数版本的 main
Kotlin 官方提供了挂起函数版本的 main()
函数,不过,挂起函数版本的 main() 的底层做了很多封装,虽然它可以帮我们省去写 runBlocking 的麻烦,但不利于我们学习阶段的探索和研究。
suspend fun main() { // 挂起函数版本的 main 函数
log("1")
withContext(Dispatchers.IO) { // 可以调用另一个 suspend 函数
log("2")
delay(1000L)
log("3")
}
log("4")
}
上面代码的打印结果是:
1 - main
2 - DefaultDispatcher-worker-1
3 - DefaultDispatcher-worker-1
4 - DefaultDispatcher-worker-1
Dispatcher 线程池
Dispatchers
是一个 object 单例,内部的成员 Default、Main、Unconfined、IO 的类型是CoroutineDispatcher
CoroutineDispatcher
实现了ContinuationInterceptor
接口ContinuationInterceptor
接口继承自CoroutineContext.Element
接口CoroutineContext.Element
接口继承自CoroutineContext
接口
所以,CoroutineDispatcher(简称 Dispatcher) 就是一个 CoroutineContext。
内置的线程池
public actual object Dispatchers {
@JvmStatic public val IO: CoroutineDispatcher = DefaultIoScheduler
@JvmStatic public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
@JvmStatic public actual val Default: CoroutineDispatcher = DefaultScheduler
@JvmStatic public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
}
- Dispatchers.Main:只在 Android、Swing 之类的 UI 平台才有意义,在普通的 JVM 工程中是无法直接使用的
- Dispatchers.Unconfined:无限制,当前协程可能运行在任意线程之上
- Dispatchers.Default:用于 CPU 密集型任务的线程池,线程个数与 CPU 核心数量一致,最小为 2
- Dispatchers.IO:用于 IO 密集型任务的线程池,线程数量一般比较多,可通过参数
kotlinx.coroutines.io.parallelism
配置
线程池间的线程复用
当 Dispatchers.Default
线程池当中有富余线程的时候,它是可以被 Dispatchers.IO
线程池复用的。
fun main() = runBlocking(Dispatchers.Default) {
log(1) // Default 线程池
withContext(Dispatchers.IO) { log(2) } // IO 任务线程池
withContext(Dispatchers.Unconfined) { log(3) } // 无限制的线程池
}
1 - DefaultDispatcher-worker-1 @coroutine#1
2 - DefaultDispatcher-worker-2 @coroutine#1
3 - DefaultDispatcher-worker-3 @coroutine#1
可以看到,三个输出代码都是运行在 Dispatchers.Default
线程池中的线程上。
自定义线程池
fun main() = runBlocking {
val dispatcher: ExecutorCoroutineDispatcher = Executors.newSingleThreadExecutor {
Thread(it, "bqt").apply { isDaemon = true }
}.asCoroutineDispatcher() // 创建了一个 CoroutineContext
log(1) // 运行在当前线程
withContext(dispatcher) { log(2) } // 自定义的线程池
withContext(Dispatchers.IO) { log(3) } // IO 任务线程池
withContext(Dispatchers.Unconfined) { log(4) } // 无限制的线程池
}
1 - main @coroutine#1
2 - bqt @coroutine#1
3 - DefaultDispatcher-worker-1 @coroutine#1
4 - main @coroutine#1
不要使用 Unconfined
Unconfined
代表的意思是,当前协程可能运行在任何线程之上,不作强制要求。
fun main() = runBlocking {
log(1)
launch {
log(2)
delay(1000L)
log(3)
}
log(4)
}
上述代码的运行顺序是:1、4、2、3
1 - main @coroutine#1
4 - main @coroutine#1
2 - main @coroutine#2
3 - main @coroutine#2
下面我们指定使用 Dispatchers.Unconfined
:
fun main() = runBlocking {
log(1)
launch(Dispatchers.Unconfined) { // 使用 Unconfined 线程池
log(2) // main
delay(1000L)
log(3) // DefaultExecutor
}
log(4)
}
上述代码的运行顺序变成了:1、2、4、3
1 - main @coroutine#1
2 - main @coroutine#2
4 - main @coroutine#1
3 - kotlinx.coroutines.DefaultExecutor @coroutine#2
所以,Unconfined 其实是很危险的,我们不应该随意使用。
withContext 切换线程池
使用 withContext()
方法,可以将当前协程的部分代码,在指定的线程池中执行。
fun main() = runBlocking {
log("1")
withContext(Dispatchers.IO) { // 指定协程代码执行的线程池
log("2")
delay(1000L)
log("3")
}
log("4")
}
1 - main @coroutine#1
2 - DefaultDispatcher-worker-1 @coroutine#1
3 - DefaultDispatcher-worker-1 @coroutine#1
4 - main @coroutine#1
可以看到,在 withContext()
中指定线程池以后,Lambda 当中的代码就会被分发到 DefaultDispatcher
线程池中去执行,而它外部的所有代码仍然还是运行在 main
线程。
其他常见的 Context
CoroutineScope 协程作用域
如果要调用 launch(),就必须先有 CoroutineScope
,即协程作用域
。CoroutineScope 只有一个成员 CoroutineContext
,所以它只是对 CoroutineContext 做了一层封装而已。
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
CoroutineScope 最大的作用,就是可以方便我们批量控制协程。
fun main() = runBlocking {
val scope = CoroutineScope(Job())
scope.launch {
log("1 start!")
delay(100L)
log("1 end!")
}
scope.launch {
log("2 start!")
delay(500L)
log("2 end!") // 不会执行
}
delay(300L)
scope.cancel()
}
2 start! - DefaultDispatcher-worker-2 @coroutine#3
1 start! - DefaultDispatcher-worker-1 @coroutine#2
1 end! - DefaultDispatcher-worker-2 @coroutine#2
这同样体现了协程结构化并发的理念。关于 CoroutineScope 更多的底层细节,我们会在源码篇的时候深入学习。
Job 协程的句柄
Job
间接实现了 CoroutineContext
接口,所以,Job 本身就是一个 CoroutineContext
。
Job 其实就是协程的句柄
,通过 Job 对象,我们主要可以监测及操控协程。Job 的具体内容上一节已经学过了。
public interface Job : CoroutineContext.Element {}
public interface CoroutineContext {
public interface Element : CoroutineContext {}
}
CoroutineName 协程的名称
CoroutineName
也间接实现了 CoroutineContext
接口,可用于指定协程的名称:
fun main() = runBlocking {
val context: CoroutineContext = CoroutineName("bqt") // 协程的名称
GlobalScope.launch(context) {
context[CoroutineName]?.let { log(it.name) }
}
log("xxx")
delay(500L)
}
xxx - main @coroutine#1
bqt - DefaultDispatcher-worker-1 @bqt#2
其中的数字
2
是一个自增的唯一ID
CoroutineExceptionHandler
CoroutineExceptionHandler
也间接实现了 CoroutineContext
接口,它主要负责处理协程当中的异常。
public interface CoroutineExceptionHandler : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
public fun handleException(context: CoroutineContext, exception: Throwable)
}
如果我们要自定义异常处理器,只需要实现 handleException()
方法即可。
fun main() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { context: CoroutineContext, throwable ->
println("${context[CoroutineName]?.name} - ${throwable.message}")
println(throwable.stackTraceToString())
}
GlobalScope.launch(CoroutineName("bqt") + exceptionHandler) {
throw Exception("自定义异常")
}.join()
}
bqt - 自定义异常
java.lang.Exception: 自定义异常
at MainKt$main$1$1.invokeSuspend(Main.kt:13)
...
CoroutineExceptionHandler 的用法看起来很简单,但当它跟协程 结构化并发
理念相结合以后,内部的异常处理逻辑是很复杂的。关于协程异常处理的机制,我们会在第 23 讲详细介绍。
挂起函数可以访问协程上下文
挂起函数可以访问协程上下文,非挂起函数不可以。
import kotlinx.coroutines.*
import kotlin.coroutines.coroutineContext // 注意不要导错包了
fun main() = runBlocking {
printInfo(1) // 1 - EmptyCoroutineContext - null
CoroutineScope(Dispatchers.IO + Job() + CoroutineName("bqt")).launch {
printInfo(2) // 2 - EmptyCoroutineContext - null
}
delay(100L)
}
// 挂起函数可以访问协程上下文
suspend fun printInfo(text: Any) =
println("$text - ${coroutineContext[CoroutineName]?.name} - $coroutineContext")
coroutineContext
返回的是当前运行作用域所对应协程的上下文信息,suspend
方法中获取的就是 runBlocking
所运行的协程所对应上下文的信息。
1 - null - [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@156643d4, BlockingEventLoop@123a439b]
2 - bqt - [CoroutineName(bqt), CoroutineId(2), "bqt#2":StandaloneCoroutine{Active}@749fe0ec, Dispatchers.IO]
小结
- CoroutineContext 是 Kotlin 协程中非常关键的一个概念。它本身是一个接口,但它的接口设计与
Map
的 API 极为相似,我们在使用的过程中,也可以把它当作 Map 来用。 - 协程里很多重要的类,它们本身都是 CoroutineContext
- 比如 Job、Deferred、Dispatcher、ContinuationInterceptor、CoroutineName、CoroutineExceptionHandler,
- 正因为它们都是 CoroutineContext,所以我们可以通过操作符重载的方式,写出更加灵活的代码
- 比如
Job() + mySingleDispatcher + CoroutineName("name")
- 协程中的
CoroutineScope
,本质上是对 CoroutineContext 的简单封装,它的能力都源自于 CoroutineContext - 协程中的
挂起函数
与 CoroutineContext 也有着紧密的联系,因为 Continuation 中就有一个 CoroutineContext 成员
2017-05-02
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/6795684.html