Kotlin 协程多线程调度程序和局部变量的线程安全

Val*_*Val 5 multithreading kotlin kotlin-coroutines

让我们考虑一下带有协程的简单代码

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")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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]]
Run Code Online (Sandbox Code Playgroud)

问题是是什么使局部变量的这些修改成为线程安全的(或者它是线程安全的?)。我知道这个循环实际上是按顺序执行的,但它可以在每次迭代时更改正在运行的线程。在第一次迭代中线程所做的更改对于在第二次迭代中拾取此循环的线程仍然应该是可见的。哪个代码可以保证这种可见性?我尝试将这段代码反编译为 Java,并使用调试器挖掘协程实现,但没有找到任何线索。

Mar*_*nik 3

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

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

如果你出于好奇想深入了解“如何”的细节,答案就变成“这取决于”。每个协程调度程序都可以选择自己的机制来实现。

作为一个指导性示例,我们可以重点关注您在发布的代码中使用的特定调度程序:JDK 的fixedThreadPoolExecutor. 您可以向该执行器提交任意任务,它将在单个(任意)线程上执行每个任务,但一起提交的许多任务将在不同线程上并行执行。

此外,执行程序服务还保证导致执行的代码发生在任务内的代码executor.execute(task) 之前,并且任务内的代码发生在另一个线程观察其完成之前(future.get()future.isCompleted()关联的线程获取事件CompletionService)。

Kotlin 的协程调度程序依靠来自执行器服务的这些原语来驱动协程完成挂起和恢复的生命周期,从而获得整个协程的“顺序执行”保证。每当协程挂起时,提交给执行器的单个任务就会结束​​,并且当协程准备好恢复时(当用户代码调用continuation.resume(result))时,调度程序会提交一个新任务。