介绍 Swift Atomics
我很高兴地宣布 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,该 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
包为排序提供了三个单独的枚举,每个枚举代表分别应用于加载、存储或更新操作的排序子集。
无锁与无等待操作
此包公开的所有原子操作都保证具有无锁实现。无锁意味着原子操作是非阻塞的——它们永远不需要等待其他线程的进度来完成自己的任务。
但是,我们不保证无等待操作:根据目标平台的功能,一些公开的操作可以通过比较和交换循环来实现。当多个线程反复竞争访问同一个原子变量时,可能会导致不公平的调度,其中一些线程可能会被其他线程反复抢占,迫使它们重试操作任意次数。尽管如此,所有原子操作都直接映射到专用的、无等待的 CPU 指令(如果可用)——在 LLVM 和 Clang 支持的范围内。
下一步是什么?
在短期内,我们希望通过添加更多原子类型和操作来完善该包,并通过改进现有的测试套件来验证我们对正确性和性能的假设。
-
标记原子 将为解决并发数据结构的常见问题提供有用的工具。这很可能建立在该库已经公开的双倍宽度原子原语之上,但发明正确的标记 API 是一项有趣的 API 设计挑战。
-
对某些 原子浮点运算 的支持是一项常见的要求。
参与进来
非常欢迎您的经验、反馈和贡献!
- 开始使用,请尝试 GitHub 上的
Atomics
库, - 在 Atomics 论坛 中讨论该库并获得帮助,
- 打开一个 issue,说明您发现的问题或您对改进的想法,
- 与往常一样,欢迎 pull requests!