Kotlin协程的Context

456 阅读7分钟

协程的 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 十分类似。

image.png

所以,我们完全可以把 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 元素之间的关系:

image.png

总的来说,Job、Dispatcher、CoroutineName,它们本质上只是 CoroutieContext 这个集合当中的一种数据类型,只是恰好 Kotlin 官方让它们都继承了 CoroutineContext 这个接口。而 CoroutineScope 则是对 CoroutineContext 的进一步封装,它的核心能力,全部都是源自于 CoroutineContext。