Revisiting SE-0282: distributing swift-atomics with the toolchain

three years ago to date we got the swift-atomics package as part of SE-0282. that proposal predated async/await and the other modern concurrency features; so it made sense for swift-atomics to be a third-party package.

now-a-days i am increasingly finding that just about any project that touches concurrency can benefit from swift-atomics’ types, since atomics really are quite fundamental to a lot of concurrent algorithms and data structures. gradually i have gained the experience that spending effort designing these kinds of things with “pure swift” concurrency primitives for the sake of avoiding a dependency is not worth it. so for me at least, i am starting to rubber-stamp what is otherwise a bureaucratic process for evaluating if a third-party dependency is justified.

right now, i do not think consuming swift-atomics as a third-party dependency is particularly painful, but it is certainly not ideal. one moderately-sized issue i am experiencing is that SPM (on docker) always re-clones the swift-atomics github repository every time i delete the .build cache, and i delete the .build directory rather often due to unrelated issues with sourcekit-lsp.

from my observations, the package evolves so slowly, and has so many potential consumers that i think we should seriously consider distributing it with the toolchain, the same way we do today with Dispatch, Foundation, etc. thoughts?

4 Likes

As a constant user who tries to teach these concepts, I would add that one huge pain point is that the Atomics library does not work in Swift Playgrounds without modification. Having it in the standard library would fix this problem and make it easier to teach.

3 Likes

i view ManagedAtomic/UnsafeAtomic as being akin to Regex. it can really supercharge a lot of tools and improve quality of life in general, but prior to StringProcessing becoming a part of the toolchain, i was really reluctant to add regex features to tools i authored, out of hesitation to add “optional” third-party dependencies.

i think if atomics shipped with the toolchain, and were available “everywhere”, that would improve the quality of concurrent data structure libraries across the board, similar to how Regex improved everyone’s tooling last year.

6 Likes

The issue here has been that doing atomics "right" requires something like move-only types (it's not quite "move-only" that's needed, but rather "stable-address without indirection", and move-only support is one way to get at that), and so we haven't wanted to go and build an atomics feature into the standard library that we end up having to deprecate a year later and telling everyone to use a different atomics feature instead.

Putting it in the toolchain but not the stdlib doesn't run up against that quite as badly, but it's a similar situation.

7 Likes

I'm a bit surprised at the notion of deprecating swift-atomics. Shocked actually. I'm going to go re-read SE-282 with a different mindset now.

I don't think that we would deprecate the package. We would put the new "better" atomics into the stdlib, and leave the package in place for any code that needs the old approach. But we want to avoid having two versions of atomics that live in the toolchain forever.

3 Likes

I think Steve was referring to if we were to commit to ManagedAtomic/UnsafeAtomic now, but a language feature lets us have real atomic values that are 1. move only and 2. have stable address then we'd be deprecating ManagedAtomic/UnsafeAtomic that we just committed to in the stdlib.

1 Like

I get that and I've just been reviewing SE-282 to verify my understanding. The problem I see with this (and admittedly I haven't given the matter much thought at this point) is that not everything will be move-only. ManagedAtomic (which is pretty much all I use) seems, prima facie, like the correct approach for anything that is ARC-managed.

And given that:

  1. move-only will be opt-in
  2. a huge part of the existing Swift world is going to be ARC for a long long time, and
  3. swift-atomics encapsulates the C/C++ model which would seem to be required for something like C++ integration

I'm not sure I see the value in tieing Atomics to move-only types. But I'm certainly willing to be educated.

1 Like

for me, my pain point is merely distributional. if the Atomics module shipped with the toolchain, the same way Foundation does today, that would solve all of my issues with swift-atomics.

i don’t think it would need to “live in the toolchain forever”, the toolchain is already full of underscored modules that blink in and out of existence all the time from my experience. but even if it did, would that really be the worst thing in the world? Dispatch becomes more and more irrelevant every day since swift concurrency became a thing, but it is still in the toolchain, and people still use it.

i imagine keeping the legacy Atomics module in the toolchain for a few minor releases would actually be very helpful for libraries that need to maintain compatibility with multiple versions of swift at a time.

1 Like

i personally use UnsafeAtomic way more often than i use ManagedAtomic. this is because i really only ever do two things with atomics:

  1. giving a class some secret mutable state while still keeping it formally Sendable, and
  2. exposing some varying property from an actor that code running outside its actor loop can read without enqueing a request on the actor and awaiting the value.

classes and actors are both allocated types, so there is always a deinit nearby that is responsible for destroying the atomics.

i personally have never had a use for ManagedAtomic, because i have never needed an atomic on its own, there is always a larger allocation to tie its lifetime to.

on the other hand, i don't think move-only stuff is going to be as game changing for atomics as @scanon suggests, because i think atomics should always have "indirection" semantics (e.g. they should always be let). having experimented a lot with atomic-backed computed properties, i've discovered that passing an atomic-backed value inout (or using any equivalent "mutating" semantics) is almost always a mistake. so i think implementing atomics as a move-only "value type" that you perform mutating operations on is incorrect.

The safe thing you would do is expose the atomic through a read accessor, so that it can’t be moved. let does accomplish this when a property is accessed directly, but not through protocol requirements or across ABI-stable module boundaries, and you can’t go through a computed property or subscript.

1 Like

is this about the (possible) race condition where you load from an UnsafeAtomic<T> whose lifetime is bound to a class/actor allocation?

let x:Int = someActorInstance._atomic.load(ordering: .relaxed)

i’ve never run into a crash from doing this, since i thought __consuming get was needed to allow someActorInstance to be deinitialized before we are done using the atomic. but then again i have been burned by this kind of thing too many times…

also, i am still quite confused by folks saying that we can store atomics inline within an actor allocation, instead of having it use a separate allocation. if the atomic is stored inline, then it is a value, and it must be var to model the fact that its value can change. but we cannot provide any nonisolated abstractions around a mutable stored property in an actor instance.

maybe we could obtain an unsafe mutable pointer to the location of the atomic storage within the actor instance’s allocation, and store to the atomic through the pointer. but this feels like it would break a lot of our usual assumptions about actors, namely that the value of a var never changes across a critical section.

Yeah, I guess you’re right. Either exclusivity applies to atomic storage (and read would count as a non-instantaneous read and wouldn’t let you violate it), or it doesn’t (and then my example is meaningless).

…but I think it’s your example that gets better: if there’s dedicated storage for an UnsafeAtomic, you could vend a non-copyable, borrowed SafeAtomic whose address is known valid for the duration of its existence, much like ideas around BorrowedBufferPointer or whatever we’re calling it. But UnsafeAtomic would still exist in that world, and ditching ManagedAtomic requires allocating storage within the class or actor and getting a stable pointer to it, as you say.

3 Likes

yes, this was my thinking as well.

we can already do this in a crude fashion for sendable classes, what is needed is a way to specify nonisolated var for a stored actor property.

2 Likes

Back in 2020, we promised that noncopyable types would be The Right Way to get atomics, locks, etc in Swift.

All of these approaches have serious shortcomings, and they're likely to be quickly superseded by move-only atomics in the future. How far that future is from the present is an open question, and having some sort of low-level API for atomic primitives could still have a small niche for advanced users even with move-only atomics as the primary interface.

After three long years, non-copyable types are now becoming a real thing. However, modeling synchronization primitives isn't a flagship use case for them; in fact, properly modeling these things continues to be an elusive thing.

It has been three years; I do not think it is tenable that Swift still does not provide such constructs.

I think concerns about not putting known-suboptimal constructs in the stdlib should come with an expiration date. The only thing we achieve by not having synchronization primitives in the Swift Standard Library at this point is the proliferation of half-baked, ill-considered attempts at implementing the same, throughout the entire ecosystem.

I deeply regret not pushing harder for including ManagedAtomic/UnsafeAtomic in the stdlib.

10 Likes

I don't think this is true. Once the implementation of SE-0390 is mature, then we're really only missing one thing necessary to model atomics or locks with move-only types, namely the ability to put a block of raw memory inside such a type. Figuring that out, either in the general case or in a special case just for atomics, would be a great flagship use for move-only types.

10 Likes

Let's make that happen! Every single day I'm getting blocked by not being able to use atomics, and the questionable alternative designs that I use to avoid them are starting to pile up too high and are getting embedded too deeply for comfort.

8 Likes

thank you Karoy, for the honest assessment of the current state of affairs with multi-platform atomics. unfortunately atomics today only work comprehensively on Apple platforms, and that is inhibiting the development of multi-platform concurrent libraries.

there are two things that can be done right now to improve the multi-platform experience for swift atomics:

  1. enabling doublewide atomics on non-Apple platforms (PR), which will allow us to use the official swift-atomics package on linux (instead of a constellation of everyone’s personal forks), and

  2. merging this PR, which i am told will allow us to use swift-atomics on Windows.

i really hope to see some movement on these items soon.

5 Likes

Ah, thanks for reminding me about that PR.

2 Likes

As Saleem pointed out, this PR would regress the installer size, so we need to rework it in some way. However, this is the correct direction. For working around the problem, see here.

FYI, if you upgraded to Visual Studio 17.5, you can build Atomics without modifying the toolchain now thanks to native stdatomic.h support.