[Second Review] SE-0410: Atomics

Hi everyone. The second review of SE-0410: Atomics begins now and runs through December 18, 2023. Here are the first review thread and language steering group decision threads. In response to the first review, the proposal has moved the new APIs into a new module, Synchronization. It also introduces a diagnostic when a variable of Atomic type is declared as a var, in order to prevent accidental misuse of atomics in ways that may incur incorrect dynamic exclusivity checking. In addition to these overall changes, the authors and language steering group would like feedback on points including:

  • In their review decision, the steering group had offered the name Atomics to contain the new APIs. This revision instead uses Synchronization, both to allow for additional synchronization-related APIs to be placed here, and to allow for the new standard API to coexist with the existing Atomics module defined by the swift-atomics package in use today. Are there other interesting names to consider for the module?
  • The language steering group had also suggested renaming the AtomicValue to AtomicWrappable to more strictly follow the API naming guidelines, but the revised proposal sticks with AtomicValue in order to reduce friction for code migrating from swift-atomics.
  • Should the WordPair type go into the Synchronization module along with atomics? If it goes into the other module, should it be renamed to DoubleWord as used in a previous proposal? One justification for the renaming to WordPair was to keep DoubleWord from showing up in code completion when developers reach for the much more common type Double, which would be less of an issue if the API must be explicitly imported. Going back to DoubleWord would again reduce the friction in moving from the APIs in the swift-atomics module to the new standard APIs.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep SE-0410 somewhere in the subject.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at https://github.com/apple/swift-evolution/blob/main/process.md

Thanks for contributing to the Swift evolution process!

Joe Groff
Review Manager

13 Likes

How is the circular dependency between Synchronization and the standard library being handled?

I still think this is extremely confusing. It would be much better to make var work as expected and ban let for atomics.

2 Likes

That notion doesn't survive any abstraction or composition over atomics. Doing that would mean that a var x: Atomic<T> acts like a let x: U when U == Atomic<T>.

Only in respect to whether dynamic exclusivity checking is involved, no? I think that’s just further evidence that atomic properties aren’t like other properties. I think it makes sense to view them as “static-borrow-only” mutable properties.

In all respects. An Atomic can be used as a nonisolated let property inside an actor like other lets. An Atomic can appear inside of a struct, and be atomically modified as part of a let value containing the struct. And so on.

In the fullness of time it also is useful to consider operations that do require exclusive access to the atomic's storage, such as tearing accesses, and a (ideally not dynamically-exclusivity-checked) var would be the way to allow those operations.

This quite frankly makes no sense to me. @Alejandro confirmed that atomic storage is inline. A let struct is immutable. It makes no sense for value of an atomic property of a struct stored in a let variable to ever change.

4 Likes

"Immutable" is really the special case of "shared access" for normal, non-atomic data. Atomics inside of otherwise immutable values make tons of sense for caching and other purposes, like mutable fields inside of C++ types (but with safety), since the containing value can still be safely shared indiscriminately while using an atomic field to store lazily-computed information related to the value.

3 Likes

No matter how useful, it is immensely confusing for values whose storage is inline with the struct to be tagged let and still be mutable.

6 Likes

There will not be a circular dependency issue whatsoever. The stdlib will just have to have its own internal watered down version of Atomic for the niche use cases it has.

This would be true, however this sort of proposal is quietly walking along the line of the whole concept of "interior mutability" (Interior mutability - The Rust Reference) which is something that helped bring inline atomics to Swift. Atomic is an address type. When you pass a value of Atomic around, you pass a pointer to it, very similar to passing a reference around with classes. Classes can mutate their storage while being just a let because like I mentioned you pass a reference around. The key difference, which is what I think is confusing, is that these things are inline instead of storing a pointer inline. When you store a value of Atomic in your struct, your struct becomes an address type who also is passed around by pointer. Because we always have an address to our atomic (and it is marked as raw storage) we can "mutate" the underlying value without needing var.

In a future proposal, we would like to introduce a type like Cell which would discuss interior mutability more in depth and introduce a way for everyone to make use of this sort of behavior.

9 Likes

The semantics of let make sense for values of reference type because they still prohibit you from modifying the reference itself. Importantly, in this example, one cannot replace the reference stored in foo.someClass even though it is a var property, because foo itself is a let:

class C {
  var name: String
}

struct Foo {
  var someClass = C(name: "default")
}

let foo = Foo()
foo.someClass.name = "modified" // ok
foo.someClass = C(name: "replacement") // error

This model is self-consistent as long as one understands that the value stored in a variable of reference type is the reference itself. In other words, (managed) references are value types of an unspeakable name. (cue spooky music :jack_o_lantern:)

The idea that a let binding could appear on the left hand side of an assignment statement is entirely new and foreign to Swift. It feels very wrong and pretty arbitrary. The proposal also doesn’t make it clear whether atomics within an immutable structure are also mutable. We could be quickly heading toward this situation:

struct Foo {
  let someAtomic = Atomic<Int>(0)
}

let foo = Foo()
foo.someAtomic.add(1) // ok
foo.someAtomic = 42 // ok? error??

I’m concerned that the proposal authors and reviewers have spent too much time very close to the implementation, leading to justifications of the above by referring to details not exposed in the surface language at all such as the ephemeral representation of an atomic binding as a pointer to its storage.

3 Likes

This is still an error. You'd have to do foo.someAtomic = Atomic(42), but even this is still an error because you cannot reassign a let.

1 Like

So I can’t reassign a let, but I can call a mutating method on one, depending on the type of the variable? Even though mutating is formally defined as “copy, mutate, and reassign to self”?

None of the mehods on Atomic are marked mutating, they are all borrowing.

1 Like

That’s not a very strong argument, IMO :wink:

My main use case for the atomics feature will involve vending instances from a pool. I will use UnsafeMutableBuffer to allocate raw memory and then placement-initialize Swift types from that memory. I will then vend borrow bindings that point into that memory region. As proposed, someone could still mutate the memory even though I only handed them an immutable reference to a type with solely let properties. This is surprising and undesirable. If I wanted to let clients modify the memory I’d vend an inout reference and mark my properties var.

2 Likes

How is this any different from existing methods on a class that can mutate that instance’s underlying storage? Using the terms loosely, you’re calling “mutating” methods on “reference” types in both scenarios.

1 Like

There’s a difference between the struct/class distinction (a distinction of “kind”) and whether a struct type conforms to Atomic.

It would be consistent with your argument to make atomic a peer of class, struct, and actor. But that’s not likely to happen.

1 Like

I don't particularly see how this relates to Atomic. You can just keep the atomic an implementation detail and thus none of your clients can change any of the underlying memory.

1 Like

The atomic property will be a property of the types vended from the pool. The client will have a borrowing reference to a type with an atomic let property. According to the proposal and to this discussion, the client can still mutate that property, not just read it. I am saying this is very bad.

1 Like

I think that’s orthogonal to this discussion - you can always wrap the atomic property in another type and vend finer-grained access control than provided out of the box, exactly the same as any other time you wish to restrict clients from calling certain methods or mutating certain properties on pre-existing types.

1 Like

Don't make the atomic property public for clients to access it? If all you want them to do is read the memory you gave them, hide the atomic property and implement your own methods that access the atomic so that there can be no "mutation".