Kotlin协程的“happens-before”保证是什么?

16

Kotlin 协程是否提供任何“先行发生”保证呢?

例如,在这种情况下,mutableVar 的写入和(潜在地)其他线程上的后续读取之间是否存在“先行发生”保证:

suspend fun doSomething() {
    var mutableVar = 0
    withContext(Dispatchers.IO) {
        mutableVar = 1
    }
    System.out.println("value: $mutableVar")
}

编辑:

也许提供更多的示例将更好地解释问题,因为它更符合Kotlin的风格(除了可变性)。这段代码是线程安全的吗:

suspend fun doSomething() {
    var data = withContext(Dispatchers.IO) {
        Data(1)
    }
    System.out.println("value: ${data.data}")
}

private data class Data(var data: Int)

请注意,当在JVM上运行时,Kotlin使用与Java相同的内存模型。 - Slaw
1
@Slaw,我知道。然而,在幕后有很多神奇的事情发生。因此,我想了解一下协程是否提供任何happens-before保证,或者这完全取决于我自己。 - Vasiliy
如果说有什么不同的话,你的第二个例子呈现了一个更简单的情况:它只是使用在withContext中创建的对象,而第一个例子则是先创建对象,在withContext内进行修改,然后在withContext之后进行读取。因此,第一个例子涉及到更多的线程安全特性。 - Marko Topolnik
...而且这两个例子只是演示了“程序顺序”方面的happens-before,这是最微不足道的一个。我在这里谈论协程的层面,而不是底层的JVM。因此,基本上,您正在询问Kotlin协程是否如此严重地出现故障,以至于它们甚至没有提供程序顺序的happens-before。 - Marko Topolnik
1
@MarkoTopolnik,如果我说错了,请纠正我,但是JLS只保证在同一线程上执行时的“程序顺序发生在之前”。现在,使用协程,即使代码看起来是顺序的,在实践中也有一些机制将其卸载到不同的线程中。 我理解你的观点“这是一个如此基本的保证,以至于我甚至不会浪费时间去检查它”(来自另一个评论),但我问这个问题是为了得到一个严格的答案。我相当确定我写的例子是线程安全的,但我想了解原因。 - Vasiliy
你必须在协程的层面上思考。在这个层面上,你编写了涉及单个协程的顺序代码(withContext只是在不同的上下文中继续执行相同的协程)。因此,你所练习的仅仅是程序顺序一致性。现在,有两个问题:1. Kotlin协程是否保证程序顺序一致性?2. 实际实现是否遵守该保证?我不确定你对这两个问题中的哪一个持怀疑态度。 - Marko Topolnik
2个回答

8
你写的代码有三个访问共享状态的地方:
var mutableVar = 0                        // access 1, init
withContext(Dispatchers.IO) {
    mutableVar = 1                        // access 2, write
}
System.out.println("value: $mutableVar")  // access 3, read

三个访问点严格按顺序排列,彼此之间没有并发。您可以放心,Kotlin的基础设施会在将控制权移交给IO线程池并返回到调用协程时建立一个“happens-before”边界。
下面是一个等效的例子,可能更具说服力:
launch(Dispatchers.Default) {
    var mutableVar = 0             // 1
    delay(1)
    mutableVar = 1                 // 2
    delay(1)
    println("value: $mutableVar")  // 3
}

由于 delay 是一个可暂停的函数,并且我们正在使用由线程池支持的 Default 调度器,行 1、2 和 3 可能在不同的线程上执行。因此,您关于先发生发生保证的问题同样适用于此示例。另一方面,在这种情况下,可以(我希望如此)完全明显,该代码的行为与顺序执行的原则一致。

Roman Elizarov,Kotlin 协程的主要作者,在一篇博客文章中提出了相同的观点。相关引述:

尽管 Kotlin 中的协程可以在多个线程上执行,但从可变状态的角度来看,它就像一个线程。同一协程中的两个操作不能同时发生。


1
谢谢。实际上是在“放心”之后的部分激励我提出这个问题。是否有任何文档链接可以阅读?或者,建立 happens-before 边缘的源代码链接也将非常有帮助(join、同步或任何其他方法)。 - Vasiliy
1
这是一个非常基本的保证,我甚至不会浪费时间去检查它。在底层,它归结为 executorService.submit(),并且有一些典型的机制等待任务完成(完成CompletableFuture或类似的内容)。从 Kotlin 协程的角度来看,这里根本没有并发。 - Marko Topolnik
1
我认为你实际上正在寻找的不是形式严谨,因为我们已经确定了你的代码是顺序的,而Kotlin协程确实可以无疑地为程序顺序排序的操作提供happens-before保证。也许你正在寻找一个终极证明,证明这个实现没有问题,但那是一个相当高的要求,而且每个新版本的Kotlin都需要重新评估。 - Marko Topolnik
2
嗯...实际上,我不认为这个线程已经确定了代码是顺序的。它肯定已经断言了。我也很想看到保证示例按预期工作而不影响性能的机制。 - G. Blake Meike
1
@Tenfour04 是的,这就是 happens-before 的工作方式,它不是由访问链定义的,而是由一系列简单的操作序列定义的。一个线程执行的所有操作在其程序顺序中彼此之前发生。因此,由于 withContext 不引入任何并发性,所以你只需要那两个从初始载体线程到 withContext 内部线程的 happens-before 边缘,然后再从内部线程返回到初始线程。但是,在这里 withContext 甚至都不是特别的,每次函数挂起时,它都可以在另一个线程中唤醒,每个人都认为这没有问题。 - Marko Topolnik
显示剩余8条评论

4

Kotlin中的协程确实提供了先行发生保证。

规则是:在协程内,挂起函数调用前面的代码先于挂起调用后面的代码执行。

你应该将协程视为普通线程:

尽管Kotlin中的协程可以在多个线程上执行,但就可变状态而言,它与线程非常相似。同一协程内的两个操作不可能是并发的。

来源:https://proandroiddev.com/what-is-concurrent-access-to-mutable-state-f386e5cb8292

回到代码示例。在lambda函数体中捕获变量并不理想,特别是当lambda是协程时。事实上,

将可变变量(var)捕获到此类块的范围内几乎总是错误的

(KT-15514的声明)

lambda之前的代码不会在lambda内部之前执行。

请参见:https://youtrack.jetbrains.com/issue/KT-15514


规则实际上是这样的:在挂起函数调用之前的代码 happens-before 挂起函数内部的代码,挂起调用后的代码也 _happens-before_。这反过来可以概括为“代码的程序顺序也是代码的 happens-before 顺序”。请注意该语句中没有任何特定于可挂起函数的内容。 - Marko Topolnik
@Marko Tololnik,我不认为是这种情况,请查看https://youtrack.jetbrains.com/issue/KT-15514。 - Sergei Voitovich
我看了一下,但是没有理解你的观点。它没有讨论可暂停函数调用的顺序。 - Marko Topolnik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接