我很高兴地宣布,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值组成,low和high,提供双宽原子基元 - 任何
RawRepresentable,其RawValue又是一个原子类型(如简单的自定义枚举类型)。 - 对选择了原子使用的类实例的强引用(通过符合
AtomicReference协议)。
特别值得注意的是对原子强引用的完全支持。这为并发数据结构提供了一个方便的内存回收解决方案,与Swift的引用计数内存管理模型完美契合。(原子强引用是以DoubleWord 操作的方式实现的)。
原子强引用的一个常见用例是创建一个懒惰地初始化(但在其他方面是恒定的)的某个类类型的变量。在这种简单的情况下,使用一般的原子引用是不合理的,所以我们也提供了一套单独的更有效的结构体(ManagedAtomicLazyReference 和UnsafeAtomicLazyReference),专门针对懒惰初始化进行优化。这可以代替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指令。
下一步是什么?
在短期内,我们希望通过增加更多的原子类型和操作来完善这个包,并通过改进现有的测试套件来验证我们对正确性和性能的假设: