swift-atomics

1.1.0

Low-level atomic operations for Swift
apple/swift-atomics

What's New

Swift Atomics 1.1.0

2023-04-03T21:50:27Z

This minor release improves support for recent Swift releases, adds new functionality and resolves some known issues.

Changes

  • This release requires Swift 5.6 or better. (Clients on older toolchains will automatically remain on the 1.0 releases.) (#67)

  • AtomicReference is now unconditionally available on all supported platforms. (#65 by @kelvin13)

  • ManagedAtomic now conforms to Sendable when its Value is Sendable. (#45)

  • RawRepresentable types can now opt into support for atomic operations on their optional values as long as their underlying RawValue also supports it. (#42 by @glessard)

    struct SomeValue: RawRepresentable, AtomicOptionalWrappable {
      var rawValue: UnsafeRawPointer
    
      init(rawValue: UnsafeRawPointer) {
        self.rawValue = rawValue
      }
    }
    
    let atomic: ManagedAtomic<SomeValue> // OK
    let atomic2: ManagedAtomic<SomeValue?> // Also OK!
  • The weakCompareExchange operation now comes with a variant that takes a single ordering (#75). This makes it less verbose to use it in the common case where the same ordering is needed on both the success and the failure paths.

    let atomic = ManagedAtomic<Int>(42)
    
    var original = 0
    var exchanged = false
    repeat {
      (exchanged, original) = atomic.weakCompareExchange(
        expected: original,
        desired: 23,
        ordering: .relaxed) // no need to specify a separate failureOrdering!
    } while !exchanged
  • DoubleWord.low and DoubleWord.high are now deprecated (#70). Use first and second instead.

  • This release lays the groundwork for replacing the use of C atomics with native Swift compiler intrinsics (#74). When it eventually becomes stable, this change will allow swift-atomics to support Windows (#56), and it will resolve a multitude of build problems on other platforms (e.g., #54, #55, #62, #8). Version 1.1 ships with experimental support for building the package in this mode; however, this mode is not enabled by default, and it is subject to arbitrarily change in any future release.

Fixes

  • ManagedAtomicLazyReference no longer leaks its value when it is deinitialized (#66).

  • AtomicReference conformances on non-final classes no longer emit a compiler warning in Swift 5.7 and later (#53). Atomic references now fully support class hierarchies, but only the superclass that introduced the AtomicReference conformance can be used as the type argument to ManagedAtomic or UnsafeAtomic. This turns a previously undiagnosed type safety violation into an unconditional compile-time error:

    class Base: AtomicReference {}
    // 1.0: warning: Non-final class 'Base' cannot safely conform to protocol
    //    'AtomicValue', which requires that 'Self.AtomicRepresentation.Value'
    //    is exactly equal to 'Self'; this is an error in Swift 6
    // 1.1: OK
    
    class Derived: Base {} 
    
    let ref1: ManagedAtomic<Base> = .init(Derived()) // OK
    let ref2: ManagedAtomic<Derived> = .init(Derived())
    // 1.0:
    //    no error, silent type safety violation
    // 1.1+: 
    //    error: 'ManagedAtomic' requires the types 'Derived' 
    //    and 'Base' be equivalent
  • All atomic operations are now implemented in terms of Int8, Int16, Int32, Int64, Int, or DoubleWord. The package no longer uses non-standard C to implement atomic operations on Bool (#37).

  • Building the package tests is now far less resource-intensive, considerably speeding up builds on some memory-constrained systems (#71).

Pull Requests

  • #42 Implement AtomicStorage for Optionals of RawRepresentable types (by @glessard)
  • #58 Fix support for non-final classes in AtomicReference (by @lorentey)
  • #65 enable AtomicReference everywhere (by @kelvin13)
  • #67 Bump minimum required toolchain to Swift 5.6 (by @lorentey)
  • #68 Remove obsolete workarounds for older toolchains (by @lorentey)
  • #69 ManagedAtomicLazyReference: Properly dispose object on deinit (by @lorentey)
  • #70 Simplify shims (by @lorentey)
  • #71 Resolve some test issues (by @lorentey)
  • #72 [test] Fix basic tests to to actually cover weakCompareExchange (by @lorentey)
  • #73 Update AtomicOptionalWrappable support for RawRepresentable types (by @lorentey)
  • #74 Add support for using LLVM intrinsics instead of C atomics (by @lorentey)
  • #76 Add a weakCompareExchange variant that only takes a single ordering (by @lorentey)
  • #77 Add an Xcode project exercising native builtins in a monomodule configuration (by @lorentey)
  • #78 Fix compatibility with Swift 5.6 (by @lorentey)
  • #80 Update file headers (by @lorentey)
  • #81 Fix some edge cases under 5.6 (by @lorentey)
  • #82 1.1 release preparations (by @lorentey)
  • #83 Utilities/run-full-tests.sh: Use a different derived data path when building the Xcode project (by @lorentey)

Full diffs: 1.0.3...1.1.0

Thank you very much to everyone who contributed to this release!

Swift Atomics ⚛︎︎

This package implements an atomics library for Swift, providing atomic operations for a variety of Swift types, including integers and pointer values. The goal is to enable intrepid developers to start building synchronization constructs directly in Swift.

Atomic operations aren't subject to the usual exclusivity rules. The same memory location may be safely read and updated from multiple concurrent threads of execution, as long as all such access is done through atomic operations. For example, here is a trivial atomic counter:

import Atomics
import Dispatch

let counter = ManagedAtomic<Int>(0)

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

The only way to access the counter value is to use one of the methods provided by ManagedAtomic, each of which implement a particular atomic operation, and each of which require an explicit ordering value. (Swift supports a subset of the C/C++ memory orderings.)

Table of Contents

Word of Warning

Atomic values are fundamental to managing concurrency. However, they are far too low level to be used lightly. These things are full of traps. They are extremely difficult to use correctly -- far trickier than, say, unsafe pointers.

The best way to deal with atomics is to avoid directly using them. It's always better to rely on higher-level constructs, whenever possible.

This package exists to support the few cases where the use of atomics is unavoidable -- such as when implementing those high-level synchronization/concurrency constructs.

The primary focus is to provide systems programmers access to atomic operations with an API design that emphasizes clarity over superficial convenience:

  • Each atomic operation is invoked in client code using a clear, unabbreviated name that directly specifies what that operation does. Atomic operations are never implicit -- they are always clearly spelled out.

  • There is no default memory ordering, to avoid accidental (and costly) use of sequential consistency. (This is surprisingly common issue in C/C++.)

  • Operations such as compare/exchange prefer to keep input values cleanly separated from results. There are no inout parameters.

Getting Started

To use Atomics in your own project, you need to set it up as a package dependency:

// swift-tools-version:5.6
import PackageDescription

let package = Package(
  name: "MyPackage",
  dependencies: [
    .package(
      url: "https://github.com/apple/swift-atomics.git", 
      .upToNextMajor(from: "1.1.0") // or `.upToNextMinor
    )
  ],
  targets: [
    .target(
      name: "MyTarget",
      dependencies: [
        .product(name: "Atomics", package: "swift-atomics")
      ]
    )
  ]
)

Features

The package implements atomic operations for the following Swift constructs, all of which conform to the public AtomicValue protocol:

  • Standard signed integer types (Int, Int64, Int32, Int16, Int8)
  • Standard unsigned integer types (UInt, UInt64, UInt32, UInt16, UInt8)
  • Booleans (Bool)
  • Standard pointer types (UnsafeRawPointer, UnsafeMutableRawPointer, UnsafePointer<T>, UnsafeMutablePointer<T>), along with their optional-wrapped forms (such as Optional<UnsafePointer<T>>)
  • Unmanaged references (Unmanaged<T>, Optional<Unmanaged<T>>)
  • A special DoubleWord type that consists of two UInt values, first and second, providing double-wide atomic primitives
  • Any RawRepresentable type whose RawValue is in turn an atomic type (such as simple custom enum types)
  • Strong references to class instances that opted into atomic use (by conforming to the AtomicReference protocol)

Of particular note is full support for atomic strong references. This provides a convenient memory reclamation solution for concurrent data structures that fits perfectly with Swift's reference counting memory management model. (Atomic strong references are implemented in terms of DoubleWord operations.) However, accessing an atomic strong reference is (relatively) expensive, so we also provide a separate set of efficient constructs (ManagedAtomicLazyReference and UnsafeAtomicLazyReference) for the common case of a lazily initialized (but otherwise constant) atomic strong reference.

Lock-Free vs Wait-Free Operations

All atomic operations exposed by this package are guaranteed to have lock-free implementations. However, we do not guarantee wait-free operation -- depending on the capabilities of the target platform, some of the exposed operations may be implemented by compare-and-exchange loops. That said, all atomic operations map directly to dedicated CPU instructions where available -- to the extent supported by llvm & Clang.

Memory Management

Atomic access is implemented in terms of dedicated atomic storage representations that are kept distinct from the corresponding regular (non-atomic) type. (E.g., the actual integer value underlying the counter above isn't directly accessible.) This has several advantages:

  • it helps prevent accidental non-atomic access to atomic variables,
  • it enables custom storage representations (such as the one used by atomic strong references), and
  • it is a better fit with the standard C atomics library that we use to implement the actual operations (as enabled by SE-0282).

While the underlying pointer-based atomic operations are exposed as static methods on the corresponding AtomicStorage types, we strongly recommend the use of higher-level atomic wrappers to manage the details of preparing/disposing atomic storage. This version of the library provides two wrapper types:

  • an easy to use, memory-safe ManagedAtomic<T> generic class and
  • a less convenient, but more flexible UnsafeAtomic<T> generic struct.

Both constructs provide the following operations on all AtomicValue types:

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,
    ordering: AtomicUpdateOrdering
) -> (exchanged: Bool, original: Value)

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

Integer types come with additional atomic operations for incrementing or decrementing values and bitwise logical operations.

func loadThenWrappingIncrement(
    by operand: Value = 1,
    ordering: AtomicUpdateOrdering
) -> Value

func loadThenWrappingDecrement(
    by operand: Value = 1,
    ordering: AtomicUpdateOrdering
) -> Value

func loadThenBitwiseAnd(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func loadThenBitwiseOr(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func loadThenBitwiseXor(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func wrappingIncrementThenLoad(
    by operand: Value = 1,
    ordering: AtomicUpdateOrdering
) -> Value

func wrappingDecrementThenLoad(
    by operand: Value = 1,
    ordering: AtomicUpdateOrdering
) -> Value

func bitwiseAndThenLoad(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func bitwiseOrThenLoad(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func bitwiseXorThenLoad(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value


func wrappingIncrement(
    by operand: Value = 1,
    ordering: AtomicUpdateOrdering
)

func wrappingDecrement(
    by operand: Value = 1,
    ordering: AtomicUpdateOrdering
)

Bool provides select additional boolean operations along the same vein.

func loadThenLogicalAnd(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func loadThenLogicalOr(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func loadThenLogicalXor(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func logicalAndThenLoad(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func logicalOrThenLoad(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

func logicalXorThenLoad(
    with operand: Value,
    ordering: AtomicUpdateOrdering
) -> Value

For an introduction to the APIs provided by this package, for now please see the first version of SE-0282.

The current version of the Atomics module does not implement APIs for tagged atomics (see issue #1), although it does expose a DoubleWord type that can be used to implement them. (Atomic strong references are already implemented in terms of DoubleWord, although in their current form they do not expose any user-customizable bits.)

Source Stability

The Swift Atomics package is source stable. The version numbers follow Semantic Versioning -- source breaking changes to public API can only land in a new major version.

The public API of version 1.1 of the swift-atomics package consists of non-underscored declarations that are marked public in the Atomics module.

By "underscored declarations" we mean declarations that have a leading underscore anywhere in their fully qualified name. For instance, here are some names that wouldn't be considered part of the public API, even if they were technically marked public:

  • FooModule.Bar._someMember(value:) (underscored member)
  • FooModule._Bar.someMember (underscored type)
  • _FooModule.Bar (underscored module)
  • FooModule.Bar.init(_value:) (underscored initializer)

Interfaces that aren't part of the public API may continue to change in any release, including patch releases.

Note that contents of the _AtomicsShims module explicitly aren't public API. (As implied by its underscored module name.) The definitions therein may therefore change at whim, and the entire module may be removed in any new release -- do not import this module directly. We also don't make any source compatibility promises about the contents of the Utilities, Tests, Xcode and cmake subdirectories.

If you have a use case that requires using underscored APIs, please submit a Feature Request describing it! We'd like the public interface to be as useful as possible -- although preferably without compromising safety or limiting future evolution.

Future minor versions of the package may introduce changes to these rules as needed.

We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. Accordingly, from time to time, new versions of this package will require clients to upgrade to a more recent Swift toolchain release. (This allows the package to make use of new language/stdlib features, build on compiler bug fixes, and adopt new package manager functionality as soon as they are available.)

Requiring a new Swift release will only require a minor version bump.

Contributing to Swift Atomics

Swift Atomics is a standalone library separate from the core Swift project. We expect some of the atomics APIs may eventually get incorporated into the Swift Standard Library. If and when that happens such changes will be proposed to the Swift Standard Library using the established evolution process of the Swift project.

This library is licensed under the Swift License. For more information, see the Swift.org Community Guidelines, Contribution Guidelines, as well as the files LICENSE.txt, CONTRIBUTING.md and CODE_OF_CONDUCT.md at the root of this repository.

Swift Atomics uses GitHub issues to track bugs and enhancement requests. We use pull requests for development.

We have a dedicated Swift Atomics Forum where people can ask and answer questions on how to use or work on this package. It's also a great place to discuss its evolution.

If you find something that looks like a bug, please open a Bug Report! Fill out as many details as you can.

To fix a small issue or make a tiny improvement, simply submit a PR with the changes you want to make. If there is an existing issue for the bug you're fixing, please include a reference to it. Make sure to add tests covering whatever changes you are making.

For larger feature additions, it's a good idea to discuss your idea in a new Feature Request or on the forum before starting to work on it. If the discussions indicate the feature would be desirable, submit the implementation in a PR, and participate in its review discussion.

Development

This package defines a large number of similar-but-not-quite-the-same operations. To make it easier to maintain these, we use code generation to produce them.

A number of source files have a .swift.gyb extension. These are using a Python-based code generation utility called gyb which we also use within the Swift Standard Library (the name is short for Generate Your Boilerplate). To make sure the package remains buildable by SPM, the autogenerated output files are committed into this repository. You should not edit the contents of autogenerated subdirectories, or your changes will get overwritten the next time the code is regenerated.

To regenerate sources (and to update the inventory of XCTest tests), you need to manually run the script generate-sources.sh in the Utilities folder of this repository. This needs to be done every time you modify one of the template files.

In addition to gyb, the _AtomicsShims.h header file uses the C preprocessor to define trivial wrapper functions for every supported atomic operation -- memory ordering pairing.

⚛︎︎

Description

  • Swift Tools 5.6.0
View More Packages from this Author

Dependencies

  • None
Last updated: Tue Jun 06 2023 21:32:39 GMT-0500 (GMT-05:00)