clojure-dev

Issues: https://clojure.atlassian.net/browse/CLJ | Guide: https://insideclojure.org/2015/05/01/contributing-clojure/
2021-02-07T16:27:17.036200Z

I may be missing something, but the mechanism that atoms use for safe concurrent access in Clojure seems to be quite poor performance in the following situation (a) high contention due to many concurrent swap! calls from multiple threads, and (b) their swap! calls return new values, i.e. they do not return the identical object their update function was given. In such cases, a different mechanism that used a lock around the "read, call update function, write new value back" would avoid any caller ever having to call its update function multiple times. The current implementation seems like it must be an explicit design decision, so I was wondering if anyone knew in what situations the current implementation is preferable? e.g. is it higher performance in the low/no contention case, perhaps?

2021-02-07T16:28:36.037400Z

Aside: I know agents can handle high contention situations better than atoms, too, but they have an asynchronous API, not a synchronous one, so aren't a drop-in replacement.

alexmiller 2021-02-07T17:43:22.040Z

I think there is an assumption of a) function to be called during swap is fast and b) often low contention

alexmiller 2021-02-07T17:44:59.041400Z

In case of uncontended swap, should be very fast (cas in jvm is I believe intrinsified down to hardware in some/most cases)

alexmiller 2021-02-07T17:47:13.043200Z

I’m not sure what you mean by your b - atoms rely on operations on immutable values so a successful op should nearly always be a nonidentical object

2021-02-07T18:14:25.044500Z

A swap! function can return the same value, i.e. no change. I doubt that is a common kind of swap! operation that people would choose to perform, but if it were done often, it would never cause other threads to re-execute their update functions.

jaihindhreddy 2021-02-07T18:15:10.045100Z

Locking for swap! would mean atoms won't comply with the epochal time model, because that way perception would impede action.

2021-02-07T18:15:43.046Z

Locking need not impede observation, though, right? It only impedes others starting their update function executions.

jaihindhreddy 2021-02-07T18:15:58.046300Z

True that.

2021-02-07T18:16:29.047400Z

I'm assuming here a scenario where the atom always contains immutable values. The locking would not be for allowing them to contain mutable values -- it would be to prevent repeated update function execution by contending threads.

jaihindhreddy 2021-02-07T18:16:34.047500Z

There's an assumption of the contained value being an actual value (immutable) in the first place

2021-02-07T18:17:57.048500Z

Locking would enable atoms to contain mutable values, and have mutating update functions, but that would definitely break the ability to observe the atom's value at arbitrary times.

jaihindhreddy 2021-02-07T18:22:04.050600Z

I've always wondered how the problem of starvation is handled with atoms. By starvation, I mean scenarios where a slow swapping fn keeps getting retried forever, while faster fns succeed. Is the answer just "make sure all your swapping fns take more or less the same amount of time"?

2021-02-07T18:25:46.053Z

There are no guarantees in the implementation of atoms that a thread will not be starved, that I am aware of. Alex's mentioned assumption above of usually low contention should enable all threads to finish, if "usually low contention" is interpreted as "there are often periods as long as the longest thread's update function when no one tries to change the atom's contents"

2021-02-07T18:26:50.054100Z

A lock-based implementation of atoms could guarantee no starvation, if the locking mechanism used guaranteed no starvation (e.g. it implemented FIFO queues of waiting threads in its implementation during contention, as opposed to something like spin locks)

2021-02-07T18:31:43.054200Z

Do Jave methods marked sychronized guarantee no starvation under contention?

2021-02-07T18:34:58.054400Z

At least a few quick Google search results on terms like "java synchronization starvation" suggest that Java makes no guarantees that a thread calling a synchronized method will eventually be granted the lock. If that is how a JVM implements synchronized, then starvation is possible from calling a highly contended synchronized method.

alexmiller 2021-02-07T18:52:42.055500Z

If you want to use locking and mutation, then that’s possible now, it’s just not what atoms are

alexmiller 2021-02-07T18:53:08.056Z

If atoms are a bad fit, then don’t use atoms

alexmiller 2021-02-07T18:54:49.056500Z

Java synchronization does not prevent starvation

alexmiller 2021-02-07T18:55:27.057600Z

Biased/fair locking defaults have changed back and forth over time as well so particular jdk versions differ in this

alexmiller 2021-02-07T18:56:04.058600Z

Java Locks do have options for fairness - you pay a lot in raw latency for that feature

💯 1
alexmiller 2021-02-07T18:57:00.059400Z

Atoms state this as a precondition

2021-02-07T18:58:58.062600Z

Understood. I'm just trying to clarify in my head (and perhaps in an article) when it is a good/bad fit, and why, and realized that an imagined alternate implementation of atoms with locking would be less prone to repeated update function calculation than the current one. Not suggesting a change in Clojure, by the way -- asking because I wanted to ensure I wasn't missing something subtle.

alexmiller 2021-02-07T18:59:10.063Z

Because swap! can retry, the value must be immutable (so optimistic changes can be discarded)

👍 1
borkdude 2021-02-07T19:00:17.064500Z

was the "pod" data structure intended to solve this problem: something like an atom but with transients (or other mutable things)?

alexmiller 2021-02-07T19:00:22.064700Z

Atom swap retries are in my experience exceedingly rare

alexmiller 2021-02-07T19:01:09.066400Z

Even stm retries are rare when I’ve used them in anger, unless creating benchmarks that deliberately s provoke retries

alexmiller 2021-02-07T19:02:21.068900Z

Atoms are most commonly used with maps - it’s possible to choose granularity by using a map holding multiple atoms. Cgrand explored this in a thing called megaref long ago

alexmiller 2021-02-07T19:03:03.069500Z

Re pods, don’t know, predates me using Clojure :)

seancorfield 2021-02-07T19:04:11.070200Z

From a discussion in beginners... are there any known problems running Clojure on a J9 JVM rather than a hotspot JVM?

Aaron Cummings 2021-02-08T22:41:39.096800Z

I ran into some weird performance issues around java.nio byte buffers on J9 which just went away after switching to Hotspot. This was probably 4 or 5 years ago, so might not still be true.

devn 2021-02-10T14:52:12.098900Z

I had a problem on j9 about 6 months ago on a cljc/cljs/clj project. I never dug in deep enough, but switching to HS made everything gravy.

ghadi 2021-02-07T19:13:10.070900Z

No

seancorfield 2021-02-07T19:16:16.071100Z

Alex has confirmed that some test failures occurred trying to run Clojure's test suite against J9 in the past.

ghadi 2021-02-07T19:17:50.071600Z

I think they’ve all been worked through

seancorfield 2021-02-07T19:20:33.071800Z

Hmm, well, the beginner was getting weird, somewhat random failures trying to run lein repl on J9 and they all went away when he switched to Hotspot so... ¯\(ツ)

ghadi 2021-02-07T19:25:23.072100Z

Link?

seancorfield 2021-02-07T20:01:46.072300Z

Discussion starts here https://clojurians.slack.com/archives/C053AK3F9/p1612718992150400 and then goes into a long thread ("I got the Java 11...") where the J9 issue was mentioned.

2021-02-07T20:57:26.073900Z

The "map holding multiple atoms" approach means giving up the ability to observe a consistent snapshot of the entire state, true? If that is an explicit part of the tradeoff of the megaref approach, versus single atom, then understood.

cgrand 2021-02-07T21:03:25.077200Z

A “mega atom” behaves like an atom but it allows for more concurrency by offering swap-in! (which has the semantics of swap! update-in)

borkdude 2021-02-07T21:06:01.077800Z

so what's the trade-off between a "meta atom" and STM/dosync?

cgrand 2021-02-07T21:32:10.089700Z

Ok, my memory was fuzzy and I mixed megaref and megaatom. So megarefs participate in the STM (through -in variants of the STM verbs). The paths space is partitioned and each partition is guarded by a regular ref, so a megaref holding a big map is less concurrent than one ref per key but more than a simple ref (and the partition count can be fine-tuned). However you can get a consistent snapshot of a megaref without a transaction (which is good in write heavy scenarios).

💯 1
cgrand 2021-02-07T21:46:57.095300Z

@andy.fingerhut my memory of the thing was fuzzy. So the thing of interest is megaref not megaatom. A megaref is an hybrid between an atom and a STM ref. It snapshots like an atom but participates in STM transactions at the path level (it’s more concurrent than a simple ref holding a map – its level of concurrency is tunable).

borkdude 2021-02-07T21:47:11.095400Z

interesting, thanks

➕ 1