干货 | Kotlin/Native 异步并发模型初探

本文介绍了 Kotlin/Native 的异步并发模型,包括基于宿主环境、Kotlin/Native 协程(单线程)以及 Worker。文章详细探讨了 Worker 的使用、对象冻结、线程安全以及预览版多线程协程的现状和问题。重点讨论了对象子图和线程安全机制,强调了在 Kotlin/Native 中正确处理并发的关键。最后,作者指出多线程协程仍处于预览阶段,建议在生产环境中谨慎使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者简介

 

禹昂,携程移动端高级工程师。Kotlin 中文社区核心成员,官方文档译者。


一、前言

作为 Kotlin Multiplatform 体系重要组成部分之一的 Kotlin/Native ,目前还是一项处于 beta 阶段的技术。而 Kotlin/Native与 Kotlin/JVM 的异步并发模型也有着极大的不同,因此如果要实践 Kotlin Multiplatform,则事先对 Kotlin/Native的异步并发模型进行探究就显得很有必要。

相较于 Kotlin/Native,Kotlin/JVM 也许为更多的人所熟知。基于 JVM 的异步并发机制,Kotlin/JVM 提供了通过编译器与线程池实现的协程来完成异步并发任务。Kotlin/JVM 的协程既能完成异步请求,也能完成并行计算,并且由于协程中拥有挂起(suspend),Kotlin/JVM 就可以在协程而非线程的层面上来解决并发竞争的问题。

即当并发竞争出现的时候,这套机制只需将协程挂起而无需阻塞线程,而对于是否发生竞争的判断可以转移到原子操作上。这样的机制避免了 JVM重量级锁的出现,个人认为这确实是 Kotlin/JVM 的协程相对于传统 JDK 中异步并发 API 的一个优势(详见文末参考文档链接 1、2)。

但 Kotlin/Native 程序作为一种原生二进制程序,相当于是重新开发的一门语言,由于没有现成的类似于 JVM 提供的异步并发机制作为依赖,所以它必须实现一套自己的异步并发模型。由于 Kotlin 在编程范式上吸收了部分函数式编程的特性,因此 Kotlin/Native 的同步方案从设计思想上向函数式编程靠拢,即对象不变性,其宗旨就是如果对象本身不可变,那就不存在线程安全的问题。

Kotlin/Native 用于实现异步和并发的方案主要有三种。

1)基于宿主环境(操作系统)实现。例如与使用 POSIX C 编写原生程序一样。直接使用相关操作系统平台提供的 API 来自己开启线程,在 POSIX 标准的系统上,手动调用 pthread_create函数来创建线程。但是这样的代码实现违反了平台通用性的原则,例如,如果你要将你的程序移植到非 POSIX 标准的系统上,那异步并发方式就得全部改用相关平台的机制,可移植性太差,在编写多平台程序的时候这种方式基本上是行不通的。

2)Kotlin/Native 自身提供给了我们两套异步并发的 API,首先是协程,但 Kotlin/Native 的协程与 Kotlin/JVM的协程区别很大,Kotlin/Native 的协程是单线程的,也就是说它只能用来执行一些不占用 CPU 资源的异步并发任务,例如网络请求。但如果要利用CPU 多核的能力来进行并行计算,Native 版的协程就失去了作用,当然,官方说了要尽快解决这个问题,并且于 2019 年 12月中已经发布了 Native 多线程版协程的预览版本,这个会在后文详细讨论。

3)除了协程之外,官方在 Kotlin/Native 诞生之初就已经提供了另一套专门做并行任务的工具,即 Worker 。Worker 与 Kotlin/Native 的异步并发模型紧密相连,做到了既能利用 CPU 多核能力,又能保障线程安全(虽然做法略微粗暴)。这篇文章我们会先介绍基于 Worker 与对象子图的现有异步并发模型,最后再讨论当前预览版本的多线程协程。

注意,本文基于 Kotlin 1.3.61,Kotlin/Native 作为一个实验性项目,任何的版本变动都有可能造成 API 的破坏性变更。

二、原生并发模型:Worker 与对象子图(Subgraph)

这部分内容,官方文档较少,目前仅有一篇(见参考链接 3),而且其内容有一定滞后性,所以本文中的部分结论可能会与该文档不符,期待后续官方更新。

Worker 与线程类似,通过打印线程 id 进行验证发现,一个 Worker 基本对应一个线程。在编写程序时,如果需要开启线程,就应该创建一个 Worker 。Kotlin/Native 对跨线程/Worker 访问对象拥有严格的限制,因此对象在一定维度上又分为两种状态,即 Freeze(冻结)与 Unfreeze(非冻结)。

冻结的对象是编译期即可证明为不可变的对象,或者是手动显式添加 @SharedImmutable 注解的对象,系统默认这类对象不可变,可以在任意的线程/Worker 中访问,而非冻结对象通常不可在创建它之外的线程/Worker 中访问。Kotlin/Native通过给对象生成对象子图(subgraph)的方式,然后在运行时遍历对象子图来检测是否发生了跨线程/Worker 访问。

2.1 对象冻结

首先创建一个基本的 Kotlin/Native 工程,本文基于 macOS 10.15.1。

对象冻结,即一个对象被创建之后即与当前线程/Worker 绑定,在不加特殊标记的情况下,在其他线程/Worker 访问该对象(无论是读还是写)就会抛出异常。但是存在另外一种对象,它们在编译期即可被证明是不可变的,这类对象就被称为冻结的对象。因此冻结对象可以在任意线程内访问,目前冻结对象有:

  • 枚举类型

  • 不加特殊修饰的单例对象(即使用 object 关键字声明的)

  • 所有使用 val 修饰的原生类型变量与 String(这种情况也就包含了 const 修饰的常量)

如果我们要将其他类型的全局变量/成员变量声明为冻结的,可以使用注解 @SharedImmutable,它可以让变量的多线程访问通过编译,但如果运行时发生了对该变量的修改,程序就会抛出 IncorrectDereferenceException 异常。除此之外,官方还表示之后可能会增加对象动态冻结的情况,也就是说一个对象一开始不是冻结的,但在运行时从某一刻开始,就变为一个冻结对象,但是无论如何,一个已被冻结的对象都是不能被解除冻结的。

2.2 Worker 的基本用法

下面我们来看看如何在 Kotlin/Native 中开启子线程进行异步计算。

在 Kotlin/Native 中我们使用 Worker 来做这件事,一个 Worker 即代表一个线程(类 Unix 系统),但在用法上却接近 Java的 Future/Promise 或 Kotlin 协程中的 async/await。与传统的 Java 中使用 Thread 的多线程编程方式相比,Worker对参数的传入以及对执行结果的获取更为严格,下面看一个例子:

fun main() {
    val worker = Worker.start(true, "worker1")
    println("Position 1, thread id: ${pthread_self()!!.rawValue.toLong()}")
    val future = worker.execute(TransferMode.SAFE, {
        println("Position 2, thread id: ${pthread_self()!!.rawValue.toLong()}")
        1 + 2
    }) {
        println("Position 3, thread id: ${pthread_self()!!.rawValue.toLong()}")
        (it + 100).toString()
    }
    future.consume {
        println("Position 3, thread id: :${pthread_self()!!.rawValue.toLong()}")
        println("Result: $it")
    }
}

使用 Worker.start 函数我们就可以创建一个新的 Worker,然后调用它的 execute函数就可以在别的线程执行任务了。这个函数接收三个参数,第一个是对象转移模式(后面会讨论),第二个参数将扮演一个生产者的角色(为了简便,后文我们使用源码中的命名 producer 来称呼它),它会在外面的线程执行,producer的返回值将在 execute 的第三个参数(也是个 lambda 表达式,同样,后文我们用源码中的命名 job 来称呼它)中作为

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值