🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## Kotlin/Native 中的并发 [TOC] Kotlin/Native 运行时并不鼓励带有互斥代码块与条件变量的经典线程式并发模型,因为已知该模型易出错且不可靠。相反,我们建议使用一系列替代方法,让你可以使用硬件并发并实现阻塞 IO。 这些方法如下,并且分别会在后续各部分详细阐述: * 带有消息传递的 worker * 对象子图所有权转移 * 对象子图冻结 * 对象子图分离 * 使用 C 语言全局变量的原始共享内存 * 用于阻塞操作的协程(本文档未涉及) ### Worker Kotlin/Native 运行时提供了 worker 的概念来取代线程:并发执行的控制流以及与其关联的请求队列。Worker 非常像参与者模型中的参与者。一个 worker 可以与另一个 worker 交换 Kotlin 对象,从而在任何时刻每个可变对象都隶属于单个 worker,不过所有权可以转移。请参见[对象转移与冻结](http://www.kotlincn.net/docs/reference/native/concurrency.html#transfer)部分。 一旦以 `Worker.start` 函数调用启动了一个 worker,就可以使用其自身唯一的整数worker id 来寻址。其他 worker 或者非 worker 的并发原语(如 OS 线程)可以使用 `execute` 调用向 worker 发消息。 ```kotlin val future = execute(TransferMode.SAFE, { SomeDataForWorker() }) { // 第二个函数参数所返回的数据 // 作为“input”参数进入 worker 例程 input -> // 这里我们创建了一个当有人消费结果 future 时返回的实例 WorkerResult(input.stringParam + " result") } future.consume { // 这里我们查看从上文例程中返回的结果。请注意 future 对象或 // id 都可以转移给另一个 worker,所以并不是必须要在 // 获得 future 的同一上下文中消费之。 result -> println("result is $result") } ``` 调用 `execute` 会使用作为第二个参数传入的函数来生成一个对象子图(即一组相互引用的对象)然后将其作为一个整体传给该 worker,之后发出该请求的线程不可以再使用该对象子图。如果第一个参数是`TransferMode.SAFE`,那么会通过图遍历来检测这一属性;而如果第一个参数是 `TransferMode.UNSAFE` 那么直接假定为 true。`execute` 的最后一个参数是一个特殊 Kotlin lambda 表达式,不可以捕获任何状态,并且实际上是在目标 worker 的上下文中调用。一旦处理完毕,就将结果转移给将会消费它地方,并将其附加到该 worker/线程的对象图中。 如果一个对象以 `UNSAFE` 模式转移,并且依然在多个并发执行子中访问,那么该程序可能会意外崩溃,因此考虑将 `UNSAFE` 作为最后的优化手段而不是通用机制来使用。 更完整的示例请参考 Kotlin/Native 版本库中的 [worker 示例](https://github.com/JetBrains/kotlin-native/tree/master/samples/workers)。 ### 对象转移与冻结 Kotlin/Native 运行时维护的一个重要的不变式是,对象要么归单个线程/worker 所有,要么不可变(*共享 XOR 可变*)。这确保了同一数据只有一个修改方,因此不需要锁定。为了实现这个不变式,我们使用了非外部引用的对象子图的概念。这是一个没有来自子图以外的外部引用的子图,(在 ARC 系统中)可由O(N) 复杂度进行算法检测,其中 N 是这种子图中元素的数量。这种子图通常是作为 lambda 表达式的结果而产生的(例如某些构建器),并且可能不含外部引用的对象。 冻结是一种运行时操作,通过修改对象头使给定的对象子图不可变,这样之后的修改企图都会抛出 `InvalidMutabilityException`。它是深度冻结,因此如果一个对象有指向其他对象的指针——这些对象的传递闭包也都会被冻结。冻结是单向转换,冻结的对象不能解冻。冻结的对象有一个很好的属性,由于其不可变性,它们可以在多个 worker/线程之间自由共享,而不会破坏“可变 XOR 共享”不变式。 一个对象是否已冻结,可以使用扩展属性 `isFrozen` 来检测,如果冻结了就可以共享。目前,Kotlin/Native 运行时只能在枚举对象创建后进行冻结,尽管将来可能实现自动冻结某些可证明不可变的对象。 ### 对象子图分离 没有外部引用的对象子图可以使用 `DetachedObjectGraph<T>`断开到 `COpaquePointer` 值的连接,该值可以存储在 `void*` 数据中,因此断开连接的对象子图可以存储在 C 语言数据结构中,并且之后还能在任意线程或 worker 中通过 `DetachedObjectGraph<T>.attach()`加回。如果 worker 机制不足以完成特定任务,那么可以将对象子图分离与[原始共享内存](http://www.kotlincn.net/docs/reference/native/concurrency.html#shared)相结合,能够在并发线程之间进行旁路对象传输。 ### 原始共享内存 考虑到 Kotlin/Native 与 C 语言之间通过互操作性的紧密联系,结合上文中提到的其他机制,可以构建流行的数据结构,如并发的 hashmap 或者与Kotlin/Native 共享缓存。可以依赖共享的 C 语言数据,并在其中存储分离的对象子图的引用。 考虑以下 .def 文件: ```c package = global --- typedef struct { int version; void* kotlinObject; } SharedData; SharedData sharedData; ``` 在运行 cinterop 工具之后,可以在版本化的全局结构中共享 Kotlin 数据,并通过自动生成的 Kotlin 代码在 Kotlin 中与其透明交互,如下所示: ```kotlin class SharedData(rawPtr: NativePtr) : CStructVar(rawPtr) { var version: Int var kotlinObject: COpaquePointer? } ``` 因此,结合上文声明的顶层变量,可以让不同的线程看到相同的内存,并使用平台相关的同步原语来构建传统的并发结构。 ### 全局变量与单例 全局变量常常是非预期并发问题的根源,因此 _Kotlin/Native_ 实现了以下机制来防止意外通过全局对象共享状态: * 全局变量(除非特别标记过)都只能在主线程(即首次初始化_Kotlin/Native_ 运行时的线程)中访问,如果其他线程访问这样的全局变量就会抛出 `IncorrectDereferenceException` * 对于标有 `@kotlin.native.ThreadLocal` 注解的全局变量,每个线程都保留线程局部副本,因此变更在线程之间并不可见 * 对于标有 `@kotlin.native.SharedImmutable` 注解的变量,其值是共享的,但是在发布之前会被冻结,因此每个线程都会看到相同的值 * 单例对象(除非标有 `@kotlin.native.ThreadLocal`)都是冻结且共享的,允许惰性值(除非企图创建循环冻结结构) * 枚举总是冻结的 结合起来,这些机制允许在多平台(MPP)项目中跨平台复用代码的自然竞态冻结编程。