如何将依赖于 ThreadLocal 的代码与 Kotlin 协程一起使用

How to use code that relies on ThreadLocal with Kotlin coroutines

一些 JVM 框架使用 ThreadLocal 来存储应用程序的调用上下文,例如 SLF4j MDC、事务管理器、安全管理器等。

但是,Kotlin 协程是在不同的线程上调度的,那么如何让它工作呢?

(问题灵感来自GitHub issue)

协程类似于 ThreadLocalCoroutineContext

要与 ThreadLocal 互操作 - 使用库,您需要实现自定义 ContinuationInterceptor 支持特定于框架的线程局部变量。

这是一个例子。让我们假设我们使用一些依赖于特定 ThreadLocal 的框架来存储一些特定于应用程序的数据(本例中为 MyData):

val myThreadLocal = ThreadLocal<MyData>()

要将它与协程一起使用,您需要实现一个上下文,以保持 MyData 的当前值,并在每次在线程上恢复协程时将其放入相应的 ThreadLocal .代码应如下所示:

class MyContext(
    private var myData: MyData,
    private val dispatcher: ContinuationInterceptor
) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
        dispatcher.interceptContinuation(Wrapper(continuation))

    inner class Wrapper<T>(private val continuation: Continuation<T>): Continuation<T> {
        private inline fun wrap(block: () -> Unit) {
            try {
                myThreadLocal.set(myData)
                block()
            } finally {
                myData = myThreadLocal.get()
            }
        }

        override val context: CoroutineContext get() = continuation.context
        override fun resume(value: T) = wrap { continuation.resume(value) }
        override fun resumeWithException(exception: Throwable) = wrap { continuation.resumeWithException(exception) }
    }
}

要在您的协同程序中使用它,您可以使用 MyContext 包装要使用的调度程序,并为其提供数据的初始值。该值将被放入恢复协程的线程上的线程本地。

launch(MyContext(MyData(), CommonPool)) {
    // do something...
}

上面的实现还会跟踪对线程本地所做的任何更改并将其存储在此上下文中,因此多次调用可以通过上下文共享“线程本地”数据。

UPDATE:从 kotlinx.corutines 版本 0.25.0 开始,直接支持将 Java ThreadLocal 实例表示为协程上下文元素。有关详细信息,请参阅 this documentation。通过 kotlinx-coroutines-slf4j 集成模块还提供了对 SLF4J MDC 的开箱即用支持。