Kotlin 原生并发实用技巧,为何?两条规则:状态码!完

2025-06-07

实用 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 演讲


  1. 是的,我确信 Rust 也包括其他东西,但在这种情况下…… 

文章来源:https://dev.to/touchlab/practical-kotlin-native-concurrency-ac7
PREV
React 中的 TypeScript 简介
NEXT
对于具有非传统背景的新开发人员来说,需要学习的最重要的 CS 原则是什么?