Kotlin协程多线程分派器和局部变量的线程安全



让我们考虑这个带有协程的简单代码

import kotlinx.coroutines.*
import java.util.concurrent.Executors
fun main() {
runBlocking {
launch (Executors.newFixedThreadPool(10).asCoroutineDispatcher()) {
var x = 0
val threads = mutableSetOf<Thread>()
for (i in 0 until 100000) {
x++
threads.add(Thread.currentThread())
yield()
}
println("Result: $x")
println("Threads: $threads")
}
}
}

据我所知这是相当合法的协程代码,它实际上产生了预期的结果:

Result: 100000
Threads: [Thread[pool-1-thread-1,5,main], Thread[pool-1-thread-2,5,main], Thread[pool-1-thread-3,5,main], Thread[pool-1-thread-4,5,main], Thread[pool-1-thread-5,5,main], Thread[pool-1-thread-6,5,main], Thread[pool-1-thread-7,5,main], Thread[pool-1-thread-8,5,main], Thread[pool-1-thread-9,5,main], Thread[pool-1-thread-10,5,main]]
问题是,是什么使得这些局部变量的修改是线程安全的(或者它是线程安全的?)我知道这个循环实际上是顺序执行的,但它可以在每次迭代中改变运行的线程。线程在第一次迭代中所做的更改对于在第二次迭代中拾取此循环的线程仍然应该是可见的。哪些代码保证这种可见性?我试图将这段代码反编译为Java,并使用调试器挖掘协程实现,但没有找到线索。

您的问题完全类似于操作系统可以在执行过程中的任何时刻挂起线程并将其重新调度到另一个CPU核心。这并不是因为所讨论的代码是"多核安全的",而是因为它保证了单个线程按照其程序顺序语义行为的环境。

Kotlin的协程执行环境同样保证了顺序代码的安全性。您应该按照这种保证进行编程,而不必担心如何维护它。

如果你想深入了解"how"出于好奇,答案变成了"视情况而定"。每个协程调度器都可以选择自己的机制来实现它。

作为一个有指导意义的例子,我们可以关注您在发布的代码中使用的特定分派器:JDK的fixedThreadPoolExecutor。你可以向这个执行器提交任意的任务,它会在单个(任意)线程上执行每一个任务,但同时提交的许多任务会在不同的线程上并行执行。

此外,执行器服务提供了保证,导致executor.execute(task)的代码发生在任务中的代码之前,而任务中的代码发生在另一个线程观察到它的完成之前(future.get(),future.isCompleted(),从相关的CompletionService获取事件)。

Kotlin的协程调度程序通过依赖于执行器服务的这些原语来驱动协程的挂起和恢复生命周期,因此你得到了"顺序执行"。保证整个协程。当协程挂起时,提交给执行器的单个任务结束,当协程准备恢复时(当用户代码调用continuation.resume(result)时),调度程序提交一个新任务。