实用 Kotlin 原生并发
为什么?
两条规则
地位
代码!!!
结尾
对于新开发者来说,Kotlin Native(以下简称“KN”)最容易让人困惑的一点就是状态和并发模型。通常来说,Kotlin 开发者都是 JVM 出身,并且期望一切都和 JVM 一模一样。但事实并非如此。
虽然有所不同,但该模型在概念上很简单。通过练习,理解它并不太难。
本系列文章是 KMP 入门套件的一部分,可在此处获取:https://go.touchlab.co/kampdevto
为什么?
Java、C++、Swift/Objc 等语言允许多个线程以不受限制的方式访问同一状态。开发者有责任避免犯错。并发问题与其他编程问题不同,它们通常很难重现。你不会在本地看到它们,但在生产环境中,在负载下,它们就会出现。
仅仅因为你的测试通过并不意味着你的代码是没问题的。
并非所有语言都如此设计。开发人员最熟悉的是 JavaScript。您可以使用 实现一定程度的并发,但无法同时引用和修改同一状态。Rust 语言专为性能和安全而构建。并发管理已融入该语言的设计中,因此它本质上类似于 C++,但减少了不受限制的共享状态1 的Worker
固有风险。
KN 规定了线程间状态共享方式。这些规则适用于 KN,但不适用于 Kotlin JVM(以下简称 KJ)。然而,Kotlin Multiplatform 的一个关键目标是确保 Kotlin 的各个版本保持源代码兼容。为了强制并发而修改语言本身(例如 Rust)虽然在某些方面很有吸引力,但会破坏对 JS 和 JVM 的支持。因此,KN 的新规则是在运行时实现和强制执行的。
两条规则
新规则在概念上很简单。
1)可变状态==1个线程
如果您的状态是可变的,则同一时间只有一个线程可以“看到”它。我们稍后会解释这意味着什么,但假设所有状态都是“可变的”,并且如果您没有进行任何并发操作,那么 KN 的使用体验与您在 Kotlin 中编写的任何其他代码几乎相同(除非您使用了顶级属性或伴随属性object
)。有关详情,请参阅第二部分中的“全局状态”。
规则 1 的目标很简单。如果只有一个线程,就不会有并发问题。从技术上讲,这被称为“线程限制”。对于原生移动和桌面 UI 开发者来说,这应该很熟悉。你不能从后台线程更改 UI。KN 已经概括了这个概念。
2)不可变状态==多线程
如果状态无法更改,则可以在线程之间共享。这在概念上也很简单。
就是这样。两条规则。
如何实现以及其含义显然更为复杂,但 KN 和并发的基本概念非常简单。
地位
简单介绍一下 KN 及其并发规则的现状。过去几年,社区不断发展壮大,为了更容易从 KJ 过渡,我们面临着放宽这些规则的压力。这种情况可能会持续一段时间,但不会彻底改变。这意味着,这些规则可能会在 2020 年稍微放宽,但现在编写的代码仍然有效。此外,我们也面临着保留这些规则并改善用户引导体验的压力。你可以说我属于这一阵营,所以写了这篇文章。总之……
代码!!!
首先,获取示例。要使用示例,您需要Intellij Community (或 Ultimate) 2019.3或更高版本。
克隆示例仓库:
克隆后,在 Intellij 中打开示例项目。
1)简单状态
首先,我们从一些基本状态开始。基本变量,可变值。其实没什么特别的,只是展示一下在没有并发的情况下是如何工作的。
在 Intellij 中打开示例项目。查找SampleMacos.kt
。在主函数中会有被注释掉的代码。查找1) Simple State
并取消注释runSimpleState()
。
在 IDE 的右侧,找到“Gradle”,然后在该窗口中找到runDebugExecutableMacos
并双击。
这是一个非常基础的示例。它演示了单线程中的状态照常可变。
fun runSimpleState(){
val s = SimpleState()
s.increment()
s.increment()
s.report()
s.decrement()
s.report()
}
class SimpleState{
var count = 0
fun increment(){
count++
}
fun decrement(){
count--
}
fun report(){
println("My count $count")
}
}
这将打印:
My count 2
My count 1
继续...
2)冻结状态
所有状态都是可变的,除非它被冻结。冻结状态对于 Kotlin 来说是一个新概念,尽管它在其他一些语言中也存在。在 Kotlin 中,freeze()
所有类上都定义了一个函数。当你调用 时freeze()
,该对象及其接触的所有内容都会被冻结且不可变。
一旦冻结,状态可以在线程之间共享,但不能改变。
在中查找“2) Frozen State” SampleMacos.kt
。取消注释freezeSomeState()
并运行它(从现在开始,“运行它”的意思是runDebugExecutableMacos
再次运行)。
fun freezeSomeState(){
val sd = SomeData("Hello 🐶", 22)
sd.freeze()
println("Am I frozen? ${sd.isFrozen}")
}
data class SomeData(val s:String, val i:Int)
你应该看到
Am I frozen? true
了解你的状态在运行时会发生改变。KN 中的每个对象都有一个标志,指示它是否处于冻结状态,而sd
我们刚刚将其翻转为 true。
返回SampleMacos.kt
,注释掉所有其他未注释的方法,并取消注释failChanges()
。再次运行。
这将失败并抛出异常。Intellij 中的输出控制台导航可能有点混乱。请确保单击顶层控制台以查看完整输出。
代码如下
fun failChanges(){
val smd = SomeMutableData(3)
smd.i++
println("smd: $smd")
smd.freeze()
smd.i++
println("smd: $smd") //We won't actually get here
}
data class SomeMutableData(var i:Int)
输出如下
smd: SomeMutableData(i=4)
Uncaught Kotlin exception: kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen sample.SomeMutableData@8b40c4b8
at 0 KNConcurrencySamples.kexe (yada yada)
at 1 KNConcurrencySamples.kexe (yada yada)
(etc)
重点来了。在冻结对象之前,你可以更改其var
值。冻结之后,如果你尝试更改其值,则会引发异常。InvalidMutabilityException
确切地说,是异常。
InvalidMutabilityException
是你的新朋友。你一开始可能感觉不到,但事实确实如此。
当您看到 时InvalidMutabilityException
,表示您正在尝试更改某个已冻结的状态。您可能不希望此状态被冻结,因此您的任务是找出它冻结的原因。请记住,当您冻结某个对象时,它所接触的所有内容都会被冻结。我们稍后会详细讨论这一点。根据我的经验,我只能说,一开始这可能会让您有些困惑。但很快就会明白。只要了解系统以及您可以使用的调试工具即可。
结尾
第一部分就到这里。我们已经安装了 IDE,并运行了一些基本的示例函数。我们还没有编写任何并发代码。第一步只是了解一些基础知识。
这些文章旨在对 KN 上的并发功能进行功能介绍。我们将跳过很多内容,只介绍您日常可能会用到的内容。如果您想深入了解,请查看Stranger Threads和我的Kotlinconf 演讲。
-
是的,我确信 Rust 也包括其他东西,但在这种情况下…… ↩