Swift Atomics——新的开源包介绍

791 阅读5分钟

我很高兴地宣布,Swift Atomics是一个新的开源包,可以在Swift代码中直接使用低级别的原子操作。这个库的目标是让无畏的系统程序员能够直接在Swift中开始构建同步结构(如并发数据结构)。

作为一个快速体验,这是使用这个新包的原子操作的样子:

import Atomics
import Dispatch

let counter = ManagedAtomic<Int>(0)

DispatchQueue.concurrentPerform(iterations: 10) { _ in
  for _ in 0 ..< 1_000_000 {
    counter.wrappingIncrement(by: 1, ordering: .relaxed)
  }
}
counter.load(ordering: .relaxed) // ⟹ 10_000_000

你可能已经注意到,这个例子中的原子操作并不遵循管理普通Swift变量的排他性规则。原子操作可以从多个并发的执行线程中执行,只要该值只通过原子操作访问。

这是由SE-0282 启用的,它是最近接受的 Swift 进化提案,明确地为 Swift 采用了 C/C++ 风格的内存模型,并(非正式地)描述了常规 Swift 代码如何与原子操作互操作。事实上,这个新包中的大多数 API 都来自 SE-0282 提案的前几个版本:它们最初是由Evolution 论坛上极富成效的合作努力开发的。我对这些讨论的所有贡献者深表感谢,我希望这个包能以同样高的精神继续进行合作

请自行承担风险

Atomics 包为原子操作提供了精心考虑的 API,它遵循 Swift API 的既定设计原则。然而,底层操作是在一个非常低的抽象层次上工作的。原子--甚至比其他低级别的并发结构--是出了名的难以正确使用。

这些API实现了Swift程序员以前无法实现的系统编程用例。特别是,原子学能够创建更高级别的、更容易使用的并发管理结构,而不需要从其他语言中导入其实现。

就像标准库中的不安全API一样,我们建议很少使用这个包--最好是完全不使用!如果有必要的话,可以使用这个包。不过,如果有必要的话,这是一个好主意:

  • 实现现有的已发布的算法,而不是发明新的算法。
  • 将原子代码隔离到小的、容易审查的单元。
  • 避免将原子结构作为接口类型来传递。

在处理原子代码时要非常谨慎。每次接触后都要使用大量的线程消毒剂。

支持的原子类型

该包为以下Swift类型实现了原子操作,所有这些类型都符合公共AtomicValue 协议:

  • 标准有符号整数类型 (Int,Int64,Int32,Int16,Int8)
  • 标准无符号整数类型 (UInt,UInt64,UInt32,UInt16,UInt8)
  • 布尔类型 (Bool)
  • 标准指针类型 (UnsafeRawPointer,UnsafeMutableRawPointer,UnsafePointer<T>,UnsafeMutablePointer<T>), 以及它们的可选包装形式 ( 如Optional<UnsafePointer<T>>)
  • 非管理性引用 (Unmanaged<T>,Optional<Unmanaged<T>>)
  • 一个特殊的DoubleWord 类型,由两个UInt 值组成,lowhigh ,提供双宽原子基元
  • 任何RawRepresentable ,其RawValue 又是一个原子类型(如简单的自定义枚举类型)。
  • 对选择了原子使用的类实例的强引用(通过符合AtomicReference 协议)。

特别值得注意的是对原子强引用的完全支持。这为并发数据结构提供了一个方便的内存回收解决方案,与Swift的引用计数内存管理模型完美契合。(原子强引用是以DoubleWord 操作的方式实现的)。

原子强引用的一个常见用例是创建一个懒惰地初始化(但在其他方面是恒定的)的某个类类型的变量。在这种简单的情况下,使用一般的原子引用是不合理的,所以我们也提供了一套单独的更有效的结构体(ManagedAtomicLazyReferenceUnsafeAtomicLazyReference),专门针对懒惰初始化进行优化。这可以代替lazy var ,在类的上下文中存储的属性,在并发的上下文中使用并不安全。

内存管理

原子访问是通过专用的原子存储表示来实现的,这些表示与相应的常规(非原子)类型保持不同。(例如,上面的计数器的实际整数值是不能直接访问的),这有几个好处:

  • 它有助于防止对原子变量的意外的非原子访问。
  • 它使某些原子值能够使用独立于其常规布局的自定义存储表示法(例如原子强引用所使用的表示法),并且
  • 它更适合于标准的C原子库,该库被用于实现实际操作。

虽然底层的基于指针的原子操作在相应的AtomicStorage 类型上被暴露为静态方法,但我们强烈建议使用更高级别的原子包装器来管理准备/处置原子存储的细节。这个版本的库提供了两种包装器类型:

  • 一个易于使用、内存安全的ManagedAtomic<T> 通用类,以及
  • 一个不太方便,但更灵活的UnsafeAtomic<T> 通用结构,带有手动内存管理。

ManagedAtomic 在这个类中,每一个原子值都需要一个类的实例分配,而且它依靠引用计数来管理内存。这使得它非常方便,但分配/引用计数的开销可能不适合每一个用例。另一方面, 可以用来对任何你能检索到指针的内存位置(适当的存储类型)进行原子操作,包括你自己分配的内存、 存储的片断,等等。作为对这种灵活性的交换,你需要手动确保指针在你访问它的时候保持有效。UnsafeAtomic ManagedBuffer

这两种结构都对所有AtomicValue 类型提供了以下的原子操作:

func load(ordering: AtomicLoadOrdering) -> Value
func store(_ desired: Value, ordering: AtomicStoreOrdering)
func exchange(_ desired: Value, ordering: AtomicUpdateOrdering) -> Value

func compareExchange(
    expected: Value,
    desired: Value,
    ordering: AtomicUpdateOrdering
) -> (exchanged: Bool, original: Value)

func compareExchange(
    expected: Value,
    desired: Value,
    successOrdering: AtomicUpdateOrdering,
    failureOrdering: AtomicLoadOrdering
) -> (exchanged: Bool, original: Value)

func weakCompareExchange(
    expected: Value,
    desired: Value,
    successOrdering: AtomicUpdateOrdering,
    failureOrdering: AtomicLoadOrdering
) -> (exchanged: Bool, original: Value)

整数类型带有额外的原子操作,用于增减值和位逻辑操作。Bool 也提供了一些相同的布尔操作。

排序枚举对应于C/C++标准中的std::memory_order ,只是这个包没有暴露消耗内存的排序。(memory_order_consume 没有被任何C/C++编译器实现,虽然它没有被明确废弃,但它的语义正在被修改,而且在当前版本的C++标准中不鼓励使用它。)Atomics 包为排序提供了三个独立的枚举,每个枚举分别代表适用于加载、存储或更新操作的排序子集。

无锁与无等待的操作

这个包所暴露的所有原子操作都保证有无锁的实现。无锁意味着原子操作是无阻塞的--它们不需要等待其他线程的进度来完成自己的任务。

然而,我们并不保证无等待操作:根据目标平台的能力,一些暴露的操作可能是通过比较和交换循环来实现的。当多个线程重复竞争对同一个原子变量的访问时,这可能会导致不公平的调度,一些线程可能会被其他线程重复抢占,迫使他们重试任意次数的操作。也就是说,在LLVM和Clang支持的范围内,所有的原子操作都直接映射到专用的、无需等待的CPU指令。

下一步是什么?

在短期内,我们希望通过增加更多的原子类型和操作来完善这个包,并通过改进现有的测试套件来验证我们对正确性和性能的假设:

  • 标签原子将提供一个有用的工具来解决并发数据结构的常见问题。这很可能是建立在库中已经暴露的双宽原子基元之上,但发明合适的标记API是一个有趣的API设计挑战。

  • 对一些原子浮点运算的支持是一个普遍要求的功能。