一. 前言
作为 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 用于实现异步和并发的方案主要有三种。
基于宿主环境(操作系统)实现。例如与使用 POSIX C 编写原生程序一样。
直接使用相关操作系统平台提供的 API 来自己开启线程,在 POSIX 标准的系统上,手动调用pthread_create
函数来创建线程,但是这样的代码实现违反了平台通用性的原则,例如,如果你要将你的程序移植到非 POSIX 标准的系统上,
那异步并发方式就得全部改用相关平台的机制,可移植性太差,在编写多平台程序的时候这种方式基本上是行不通的。Kotlin/Native 自身提供给了我们两套异步并发的 API,首先是协程,但 Kotlin/Native 的协程与 Kotlin/JVM
的协程区别很大,Kotlin/Native 的协程是单线程的,也就是说它只能用来执行一些不占用 CPU 资源的异步并发任务,例如网络请求。但如果要利用
CPU 多核的能力来进行并行计算,Native 版的协程就失去了作用,当然,官方说了要尽快解决这个问题,并且于 2019 年 12
月中旬已经发布了 Native 多线程版协程的预览版本,这个会在后文详细讨论。除了协程之外,官方在 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 4, thread id: :${pthread_self()!!.rawValue.toLong()}")
println("Result: $it")
}
}
使用 Worker.start
函数我们就可以创建一个新的 Worker,然后调用它的 execute
函数就可以在别的线程执行任务了。这个函数接收三个参数,第一个是对象转移模式(后面会讨论),
第二个参数将扮演一个生产者的角色(为了简便,后文我们使用源码中的命名 producer 来称呼它),它会在外面的线程执行,producer
的返回值将在 execute
的第三个参数(也是个 lambda 表达式,同样,后文我们用源码中的命名 job 来称呼它)中作为参数来提供。
而 job 中的代码会在别的线程中执行。最后 execute
函数的返回结果是一个 Future<T>
类型的对象,调用它的成员函数 consume
即可在外部线程获得 job 执行的结果。
为了验证代码中的几个关键位置到底是在哪个线程中执行的,我们使用 posix 标准中的 pthread_self()
函数打印线程 id,这段代码执行后的输出如下:
Position 1, thread id: 4524555712
Position 2, thread id: 4524555712
Position 3, thread id: 123145337905152
Position 4, thread id: 4524555712
Result: 103
我们可以看到,位置 1、2、4 三处的线程 id 打印结果相同,即 producer、以及取得计算线程执行结果的
consume
函数都在外部线程执行,而位置 3 打印的线程 id 与其他三处都不同,也就是说 job 是在后台线程中执行。
以上就是 Worker 的基本用法,但这其中有几个点需要注意,job 作为一个 lambda 表达式,不能随意捕捉上下文中的变量,
进入 job 的参数必须从 producer 传入(producer 的返回值即为 job 的参数)。考虑一种情况,如果我们在主线程中得到了一个结果,
然后想将它传递给 Worker,很自然的我们可能会写出如下代码:
fun main() {
val worker = Worker.start(true, "worker1")
val testData = TestData()
val future = worker.execute(TransferMode.SAFE, { testData }) {
it
}
future.consume { println(it.index) }
}
data class TestData(var index: Int = 0)
但这段代码会在运行时抛出 IncorrectDereferenceException
异常,因为 testData
虽然是用 val
修饰的,但它不是 String
或原生类型,因此它不是一个被冻结的对象。仔细分析一下这段代码,在主线程中 testData
对象初始化之后,紧接着会执行 producer 内的代码,当 producer 执行完毕后,异步的 job
内的代码就会开始执行,但是主线程依然可以引用到 testData
,这时就会发生并发访问的问题。那么如何避免这个问题?修改代码:
fun main() {
val worker = Worker.start(true, "worker1")
var testData: TestData? = TestData()
val future = worker.execute(TransferMode.SAFE, {
val result = testData!!
testData = null
result
}) {
it
}
future.consume { println(it.index) }
}
data class TestData(var index: Int = 0)
我们只需在 producer 返回前解除对需要传递的对象的引用,代码就可以正常运行,但上面这段代码只是一个为了便于理解的例子,
在真正的软件开发当中,我们只需要将需要传递的值不向 producer 作用域之外暴露即可。
现在我们回过头来看看 execute
的第一个参数,它代表对象转移校验模式,是一个枚举类型,共有 SAFE
与 UNSAFE
两个值可选,在上面的示例中,我们都使用的是 SAFE
模式,现在我们把它更换为 UNSAFE
模式并编写一个典型的并发写程序:
fun main() {
val worker = Worker.start(true, "worker1")
val testData = TestData()
val future = worker.execute(TransferMode.UNSAFE, { testData }) { data ->
repeat(20000) { data.index++ }
data
}
repeat(20000) { testData.index++ }
future.consume { println(it.index) }
}
data class TestData(var index: Int = 0)
在 UNSAFE
模式下,testData
作为一个非冻结的对象也能任意传递到子线程中,如果这段代码中的线程调用是安全的,
那么最终打印输出的结果应该是 40000,但很可惜,如果多次运行这段代码,每次它的打印输出结果都会不同,且小于
40000。也就是说 UNSAFE
模式下,Worker 不做任何线程安全的校验(无论是编译期还是运行时)。
这个结论与我预先猜测的不同,在源代码的注释中,对于 UNSAFE
是这样描述的:
"Skip reachibility check, can lead to mysterious crashes in an application."。
所以我预先猜测的是,如果没有发生事实上的多线程竞争,程序会正常运行,但是一旦发生多线程竞争,程序会抛出异常并崩溃。
但测试结果却不是这样,一旦使用 UNSAFE
模式,代码就变得和在 Java 中编写不加任何同步机制的并发访问代码一样不安全,
任何的潜在风险都不会被显式的表现出来,因此 UNSAFE
模式的注释中,官方也写了下面这句话:
"USE UNSAFE MODE ONLY IF ABSOLUTELY SURE WHAT YOU'RE DOING!!!"。
在这里我给出的建议是,如果能用语言机制规避的风险,就不要交给"人",因此,在 99.99% 的情况下,都应该尽量使用 SAFE
模式,虽然 SAFE
模式对于对象的传递在语法上有更严格的限制,但是如果为了图方便使用 UNSAFE
,在代码发生修改之后的潜在风险非常之大。
2.3 对象子图
这一小节主要讨论一个概念,即我们该怎样理解 Kotlin/Native 是如何检测一个对象是否在多个线程/Worker 中是可访问的?
在官方文档中提到了对象子图(subgraph)的概念,详见参考链接 3。
但是由于其资料较少,以下是我的个人理解:
"在我们使用 Worker 的时候, Worker 会将 producer 返回的对象进行包装,生成一个对象子图(subgraph),
我们可以将对象子图理解为一个对象,或是将它理解为一个对象头(因为这看起来有点类似在 TCP/IP
报文头上添加 HTTP 报文头的感觉),它与原对象相互引用。每次在线程中访问对象的时候,
都会通过 O(N) 复杂度的算法(官方未说明具体算法)来检测该对象是否在多个线程内可见。
上面讨论的对象冻结,也是通过对象子图来实现的。"
对象子图在某些特殊的情况下可以与对象分离,从而让我们可以自由的让对象在多个线程间访问,这虽然不安全,
但也是如果我们要使用其它同步机制(例如一些平台相关的同步机制或协程的 Mutex)必须要进行的步骤,有关对象子图分离的内容将在
3.3 小节与协程的 Mutex 一起详细介绍。
2.4 单例与全局变量
对于单例与全局变量来说(成员变量也类似),在 Worker 中对其进行直接的访问是无法避免的,我们不能每次都通过 producer
将单例或全局变量传递给 Worker 之后就将其置空,因此在 Kotlin/Native 中,单例与全局变量有着特别的规则。
先来介绍一下 @ThreadLocal
注解,编写一个示例:
@ThreadLocal
val testData = TestData()
fun main() {
val worker = Worker.start(true, "worker1")
val future = worker.execute(TransferMode.UNSAFE, {}) {
println(++testData.index)
}
future.consume { println(testData.index) }
}
data class TestData(var index: Int = 0)
运行这段代码的输出如下:
1
0
被添加了 @ThreadLocal
注解的全局变量会在每个线程中维护一个单独的副本,即在线程中对其进行修改对于其他线程是不可见的。
在上面这个例子中,我们在 Worker 内对 testData.index
进行了自增操作,然而在主线程中则感知不到它的变化。
我们在讨论对象冻结的时候提到过 @SharedImmutable
注解,现在我们使用 @SharedImmutable
替换 @ThreadLocal
然后运行程序,程序崩溃并抛出 InvalidMutabilityException 异常,如果我们再将 ++testData.index
这一行中的 ++
去掉,程序正常运行,这说明,对于开发者"手动"冻结的对象,并发的读取不会有问题,但是一旦其中一个线程/Worker
要对变量进行修改,就会抛出 InvalidMutabilityException 异常。
对于单例(使用 object
关键字声明的),在不加任何特别注解的情况下,它都是冻结的,你可以认为它是一个默认添加了 @SharedImmutable
注解的全局变量,但如果有特别的需要,也可以给单例添加 @ThreadLocal
注解,让它变成一个线程局部的可变变量,关于单例的代码示例不再给出。
三. 预览版的多线程协程
在上面的章节中,我们介绍的 Worker 与对象子图是在 Kotlin/Native 在诞生之初就已经定型的异步并发模型,而 Kotlin/Native
上的协程长久以来都只支持单线程,这就使得 Native 版的协程相对于 JVM 版功能大打折扣,但好消息是,近期在协程的官方 Github
仓库(kotlinx.coroutines)的 issue#462(参考链接 5)
中,Kotlin 官方团队的 Roman Elizarov 提到了已经发布了第一个多线程协程的预览版本,这也让 Kotlin/Native
的开发者们看到了官方支持多线程协程的决心。但需要说明的是,当前多线程版本的协程仅仅是一个早期预览版,从目前的体验情况来看,
后续的改动一定会不小,因此本文仅仅是做一个尝试,Native 上的多线程协程的最终形态还要等正式版推出之后才能确定。
若要导入当前主分支版本的协程,可以添加如下依赖:
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.3"
}
如果您想尝鲜预览版的多线程协程,则可以添加如下依赖:
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.3-native-mt"
}
3.1 Default 与 Main 调度器的指向发生破坏性变更
在主分支的协程中,Dispatchers
下的两个调度器 Dispatchers.Main
与 Dispatchers.Default
指向同一个线程,即主线程(程序最初初始化的线程)。而在多线程版的协程中 Dispatchers.Default
变更为指向一个后台单线程,我们通过如下代码示例即可验证:
fun main() {
println("Position 1, thread id: ${pthread_self()!!.rawValue.toLong()}")
GlobalScope.launch(Dispatchers.Default) {
println("Position 2, thread id: ${pthread_self()!!.rawValue.toLong()}")
}
GlobalScope.launch(Dispatchers.Main) {
println("Position 3, thread id: ${pthread_self()!!.rawValue.toLong()}")
}
CFRunLoopRun() // Create Darwin main thread loop
}
- 注意,
Dispatchers.Default
是单线程而不是多线程组成的线程池的说法详见参考链接 4,可自行验证。
输出打印如下:
Position 1, thread id: 4664880576
Position 2, thread id: 123145451188224
Position 3, thread id: 4664880576
如打印结果所示,位置 1 与 3 的线程 id 相同,而位置 2 则与前面两者不同,这说明了经 Dispatchers.Default
调度的协程运行在一个后台线程中。在这里 main
函数体与经 Dispatchers.Main
调度后的协程都运行在主线程内。不过这里有一点需要注意 Dispatchers.Main
调度器在所有 Darwin(即全部 Apple 平台:iOS、macOS、watchOS、tvOS 等等)上调度方式改用了平台相关的
RunLoop,在上面的示例中,我们使用 CFRunLoopRun
函数开启了主线程循环,所以 Dispatchers.Main
调度器才会有效,如果我们使用协程的 runBlocking
函数开启主线程循环,则 Dispatchers.Main
调度器在 Darwin 平台上将失效。考虑以下代码示例:
fun main() = runBlocking {
launch(Dispatchers.Main) {
println("Run on the main thread")
}
Unit
}
上面这段代码在主分支的协程中所有的 Native 平台上都可以正常打印,但在多线程版协程中,如果目标平台为
Darwin,则协程内部的打印输出将永远不会生效,但在 Linux、Windows 等平台上仍可以正常打印。这实际上是一个进步,
如果我们要编写移动端的多平台程序,我们会更希望 Dispatchers.Main
在 iOS 上切换到 UI 主线程。
3.2 利用 CPU 多核能力的主要方式:newSingleThreadContext() 函数
Dispatchers.Default
调度器虽然可以将您当前在协程中执行的异步代码切换到后台线程,但它与 Kotlin/JVM
上的 Dispatchers.Default
线程池实现相比,仍然力有不足。如果您想充分利用 CPU 的多核性能,Native 的 Dispatchers.Default
仍然不能满足您的需求。但是当前预览版本的多线程协程中仍然没有线程池的实现,因此我们必须手动创建其他的多线程上下文。
在主分支版本的协程上,程序无法引用到 newSingleThreadContext()
函数,它曾经是 Kotlin/JVM
独有的,但当前 Kotlin/Native 的预览版的多线程协程中,newSingleThreadContext()
是我们使用 CPU 多核能力的主力调度器,见如下代码示例:
@UseExperimental(ExperimentalCoroutinesApi::class)
fun main() = runBlocking {
println(pthread_self()!!.rawValue.toLong())
launch(newSingleThreadContext("1")) {
println(pthread_self()!!.rawValue.toLong())
}
launch(newSingleThreadContext("2")) {
println(pthread_self()!!.rawValue.toLong())
}
Unit
}
输出打印如下:
4703317440
123145445687296
123145446223872
每一个 newSingleThreadContext()
都会创建一个新的线程,所以真正正确的用法是我们每次都应该把 newSingleThreadContext()
创建的 CoroutineContext 保存起来然后重复使用,当我们不再需要一个由 newSingleThreadContext()
产生的 CoroutineContext 时,我们应该手动将其回收以释放资源,如下所示:
@UseExperimental(ExperimentalCoroutinesApi::class)
fun main() = runBlocking {
println(pthread_self()!!.rawValue.toLong())
val coroutineContext = newSingleThreadContext("1")
val job = launch(coroutineContext) {
println(pthread_self()!!.rawValue.toLong())
}
job.join()
coroutineContext.close()
}
此外,由于 Kotlin/Native 中积极推行 Worker 取代线程的概念,因此通过 newSingleThreadContext()
产生的 CoroutineContext 可以直接通过成员属性 worker
引用到该线程对应的 Worker,如下所示:
@UseExperimental(ExperimentalCoroutinesApi::class)
fun main() = runBlocking {
println(pthread_self()!!.rawValue.toLong())
val coroutineContext = newSingleThreadContext("1")
val job = launch(coroutineContext) {
println(pthread_self()!!.rawValue.toLong())
}
job.join()
coroutineContext.worker.execute(TransferMode.SAFE, {}) {
"Hello Multi-thread"
}.consume {
println(it)
}
coroutineContext.close()
}
3.3 对象子图分离与失效的 Mutex
协程构建器(例如 launch
、async
等)的参数 lambda 表达式可以任意捕捉上下文变量,它将默认捕捉的变量都是冻结的(这里指的是局部变量),
即,如果协程所运行的线程与外部线程不同,且如果发生修改这些捕捉过来的变量时,则程序都会抛出 InvalidMutabilityException 异常。
但是在协程中,我们有协程自己的基于挂起实现的锁 Mutex,因此如果要使用 Mutex 来保证并发安全,第一步要做的就是让变量的更改摆脱
Worker-对象子图机制,完全将并发风险暴露出来,然后才能通过将有风险的代码包裹在 Mutex 锁的作用域内来充分利用 Mutex。
然而,在协程构建器与 Worker 的 execute
函数不同,不能将协程本身设置为 UNSAFE
模式,因此这里需要将对象子图暂时分离,
然后在协程构建器内再将其重新绑定。用法如下面的代码示例所示:
fun main() = runBlocking {
val testData = TestData()
val bareTestData = DetachedObjectGraph(TransferMode.UNSAFE) { testData }
val job = launch(Dispatchers.Default) {
val outTestData = bareTestData.attach()
repeat(20000) { outTestData.index++ }
}
repeat(20000) { testData.index++ }
job.join()
println(testData.index)
}
data class TestData(var index: Int = 0)
为了便于理解代码,我们可以用下图更直观的解释对象子图,以及对象子图分离的过程:
[图片上传失败...(image-d9cbfc-1583720793912)]
虽说叫做对象子图分离,但是在用法上却更类似于包装,我们使用 DetachedObjectGraph<T>
类来包装一个对象,即可实现对象子图分离。DetachedObjectGraph<T>
的构造函数接收两个参数,第一个是对象转移校验模式
TransferMode,可以看到,如果要达成我们的目的,这里必须使用 UNSAFE
模式,第二个参数则类似于 execute
函数的 producer。然后我们在需要使用它的协程中再调用 DetachedObjectGraph<T>
类的扩展函数
attach
,即可以拿到原对象。DetachedObjectGraph<T>
类的另一个构造函数重载接收一个 COpaquePointer?
类型的参数(代表一个指针),感兴趣的读者可以自行尝试。
这段代码的运行后的打印输出结果与上文展示的 execute
函数的 UNSAFE
模式如出一辙,最终输出的值一定小于 40000(如果并发安全的话会输出 40000 整)。
然后,我们将上面的代码添加到协程的并发安全机制 Mutex 中来,示例代码如下所示:
fun main() = runBlocking {
val testData = TestData()
val bareTestData = DetachedObjectGraph(TransferMode.UNSAFE) { testData }
val mutex = Mutex()
val job = launch(Dispatchers.Default) {
val outTestData = bareTestData.attach()
repeat(20000) {
mutex.withLock { outTestData.index++ }
}
}
repeat(20000) {
mutex.withLock { testData.index++ }
}
job.join()
println(testData.index)
}
很可惜,当前预览版的多线程协程的 Mutex 存在 bug,一旦两个协程发生事实上的 Mutex 锁竞争,Mutex 就会将协程一直挂起而不恢复,
这会导致我们永远看不到输出结果,如果将上面的代码剔除掉与 Native 有关的部分(例如对象子图分离),然后拿到 Kotlin/JVM
上运行,可以正常得到输出:"40000",剔除与 Native 相关部分的代码如下所示:
fun main() = runBlocking {
val testData = TestData()
val mutex = Mutex()
val job = launch(Dispatchers.Default) {
repeat(20000) {
mutex.withLock { testData.index++ }
}
}
repeat(20000) {
mutex.withLock { testData.index++ }
}
job.join()
println(testData.index)
}
这说明 Mutex 的功能在后续有待修复。
除了 Mutex 外,官方还有另一种建议使用的实现并发安全的机制——基于 actor
协程构建器与 Channel
的消息机制。但该机制由于目前 actor
协程构建器在 Kotlin/Native 上不可用也暂时无济于事。
四. 总结与个人观点
在本文中我们一共体验了两套 Kotlin/Native 中实现异步与并发的方式,Worker-对象子图模式虽然可以确保并发安全,
但是其做法较为粗暴,但目前来说 Worker-对象子图模型仍然是较为成熟的一套实现异步与并发的机制。
多线程版的协程由于处在预览版,因此问题也非常的多,目前已知的问题包括:
- 1.
Dispatchers.Default
调度器功能有限,与 Kotlin/JVM 版的差距太大,但官方资料(参考链接 4)提到后续Dispatchers.Default
有可能会变更为多线程版本。 - 2.基于协程挂起实现的锁 Mutex 存在 Bug,当前会造成协程的长时间挂起且不恢复。
- 3.官方资料(参考链接 4)中提到,当前预览版的多线程协程存在内存泄漏。
- 4.由于
Dispatchers.Default
与Dispatchers.Main
调度器指向的线程发生了破坏性变更,如果您之前已经在工程中使用了主分支的单线程版线程,可能会面临代码迁移的问题。
当然,协程与已存在的 Worker-对象子图模型之间也并不协调,就如同上面的示例,如果要使用协程的并发安全机制保证并发安全,
就必须进行对象子图分离。然而对象子图的概念在 Kotlin/JVM 上并不存在,这会导致使用协程编写的代码不能做到真正的平台无关。
从长远来看,协程-挂起机制是 Kotlin 的核心,如果后续 kotlinx.io
库完整实现了基于 suspend
的 I/O,那么协程就可以一统
Kotlin 上的所有异步并发场景,因此,Worker-对象子图模型与多线程的协程之间会如何调和的更优雅,还有待官方后续的完善。
当前,Kotlin/Native 已经经过了接近三年左右的实验性阶段,进入了一个"相对稳定"的状态,据说 2020 年发布的 Kotlin 1.4
会让 Kotlin/Native 进入正式版,如果想要试验 Kotlin/Native
在线上产品中是否可行,个人认为,只要经过大量且完备的测试(虽然做起来并不容易),以目前状况来看,是值得一试的,
但预览版的多线程协程则不同,它处在一个非常非常早期的预览阶段,想要在线上产品中使用,还要等待官方后续推出更加稳定的版本。
五. 参考
参考链接 1:Kotlin 编译器实现协程的主要工作是 CPS 变换与状态机,官方 KEEP:
https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md参考链接 2:Java 计划在 JDK 15 中添加类似协程的异步并发工具,即 Project Loom:
https://wiki.openjdk.java.net/display/loom/Main#Main-Design参考链接 3:Kotlin/Native 关于异步并发模型的官方文档:
https://kotlinlang.org/docs/reference/native/concurrency.html参考链接 4:Roman Elizarov 编写的关于多线程版 Native 协程的官方资料:
https://github.com/Kotlin/kotlinx.coroutines/blob/native-mt/kotlin-native-sharing.md参考链接 5:关于 Native 多线程协程的 issue:issue#462
https://github.com/Kotlin/kotlinx.coroutines/issues/462