End

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 + list2map3 = 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

posted @ 2017-05-02 13:25  白乾涛  阅读(14376)  评论(0编辑  收藏  举报