• 带有消息传递的 worker
    • 对象子图所有权转移
    • 对象子图冻结
    • 对象子图分离
    • 用于阻塞操作的协程(本文档未涉及)

    Kotlin/Native 运行时提供了 worker 的概念来取代线程:并发执行的控制流以及与其关联的请求队列。Worker 非常像参与者模型中的参与者。一个 worker 可以与另一个 worker 交换 Kotlin 对象,从而在任何时刻每个可变对象都隶属于单个 worker,不过所有权可以转移。 请参见部分。

    一旦以 函数调用启动了一个 worker,就可以使用其自身唯一的整数 worker id 来寻址。其他 worker 或者非 worker 的并发原语(如 OS 线程)可以使用 execute 调用向 worker 发消息。

    调用 execute 会使用作为第二个参数传入的函数来生成一个对象子图 (即一组相互引用的对象)然后将其作为一个整体传给该 worker,之后发出该请求的线程不可以再使用该对象子图。如果第一个参数是 TransferMode.SAFE,那么会通过图遍历来检测这一属性;而如果第一个参数是 TransferMode.UNSAFE 那么直接假定为 true。 execute 的最后一个参数是一个特殊 Kotlin lambda 表达式,不可以捕获任何状态, 并且实际上是在目标 worker 的上下文中调用。一旦处理完毕,就将结果转移给将会消费它地方,并将其附加到该 worker/线程的对象图中。

    如果一个对象以 模式转移,并且依然在多个并发执行子中访问, 那么该程序可能会意外崩溃,因此考虑将 UNSAFE 作为最后的优化手段而不是通用机制来使用。

    Kotlin/Native 运行时维护的一个重要的不变式是,对象要么归单个线程/worker 所有,要么不可变(共享 XOR 可变)。这确保了同一数据只有一个修改方,因此不需要锁定。为了实现这个不变式,我们使用了非外部引用的对象子图的概念。 这是一个没有来自子图以外的外部引用的子图,(在 ARC 系统中)可由 O(N) 复杂度进行算法检测,其中 N 是这种子图中元素的数量。 这种子图通常是作为 lambda 表达式的结果而产生的(例如某些构建器),并且可能不含外部引用的对象。

    冻结是一种运行时操作,通过修改对象头使给定的对象子图不可变, 这样之后的修改企图都会抛出 InvalidMutabilityException。它是深度冻结,因此如果一个对象有指向其他对象的指针——这些对象的传递闭包也都会被冻结。 冻结是单向转换,冻结的对象不能解冻。冻结的对象有一个很好的属性, 由于其不可变性,它们可以在多个 worker/线程之间自由共享, 而不会破坏“可变 XOR 共享”不变式。

    一个对象是否已冻结,可以使用扩展属性 isFrozen 来检测,如果冻结了就可以共享。目前,Kotlin/Native 运行时只能在枚举对象创建后进行冻结,尽管将来可能实现自动冻结某些可证明不可变的对象。

    没有外部引用的对象子图可以使用 DetachedObjectGraph<T> 断开到 COpaquePointer 值的连接,该值可以存储在 数据中,因此断开连接的对象子图可以存储在 C 语言数据结构中,并且之后还能在任意线程或 worker 中通过 DetachedObjectGraph<T>.attach() 加回。如果 worker 机制不足以完成特定任务,那么可以将对象子图分离与原始共享内存相结合,能够在并发线程之间进行旁路对象传输。

    在运行 cinterop 工具之后,可以在版本化的全局结构中共享 Kotlin 数据, 并通过自动生成的 Kotlin 代码在 Kotlin 中与其透明交互,如下所示:

    因此,结合上文声明的顶层变量,可以让不同的线程看到相同的内存, 并使用平台相关的同步原语来构建传统的并发结构。

    全局变量常常是非预期并发问题的根源,因此 Kotlin/Native 实现了以下机制来防止意外通过全局对象共享状态:

    • 全局变量(除非特别标记过)都只能在主线程(即首次初始化 Kotlin/Native 运行时的线程)中访问,如果其他线程访问这样的全局变量就会抛出 IncorrectDereferenceException
    • 对于标有 @kotlin.native.ThreadLocal 注解的全局变量,每个线程都保留线程局部副本, 因此变更在线程之间并不可见
    • 对于标有 @kotlin.native.SharedImmutable 注解的变量,其值是共享的,但是在发布之前会被冻结,因此每个线程都会看到相同的值
    • 枚举总是冻结的

    结合起来,这些机制允许在多平台(MPP)项目中跨平台复用代码的自然竞态冻结编程。