"One might have thought that every use of atomic-invoke would produce its own mutex, so only atomic-invokes on the same thunk would be thus restricted, but it would seem not."
I never thought of that. I suppose that would have been possible to do by constructing a mutex during macroexpansion.
(self-atomic:foo bar baz)
==>
(self-atomic-invoke '#<mutex> (fn () (foo bar baz)))
...where #<mutex> is a new mutex value
I don't see how this would have accomplished much, though. While self-atomic may protect the thunk from itself, it doesn't help two lexically separate parts of the code.
It does mean someone can easily construct multiple GILs.
(def my-gil-1 (func)
(self-atomic:func))
(def my-gil-2 (func)
(self-atomic:func))
(= my-gil-3 [self-atomic:_]) ; same result
If they use eval, they can construct mutexes on the fly (even without a language-provided constructor utility):
(def make-mutex ()
(eval '[self-atomic:_]))
So I suppose that alternate world isn't too bad. :-p
---
"Well, why not remove such usage of atwith and atwiths inside places and setforms, and note that one must not assume that assignments made that way are atomic, and let the programmer use atomic-invoke or other more flexible synchronization primitives oneself where this is desirable?"
For what it's worth (noting again that I haven't used threads), that's been my preference for a long time, because...
"Or better yet, provide alternate versions of =, swap, push, rotate, etc. that are guaranteed atomic?"
...there's no need for these as alternatives. We can already say (atomic:push ...) if and when we need to.
On the other hand, if these alternatives were to let their subexpressions finish evaluating before they began their lock, something like this...
...then we'd have a totally different option than (atomic:push ...), and I expect it to be closer to what we intend to express with (atomic:push ...) anyway. A finer-grained (synchronized barval (scar ...)) would probably be even closer.