The process of creation of some of Scala immutable collections, most notably List, is mutable. In 2.13 the concurrency issues were addressed by adding a release fence basically to every builder. A release fence prevents memory writes coming after the fence from being reordered before the fence - so, if you have a 'final write' (such as 'payload_ready' in many examples), it means that anything you wrote (and read) before is commited to shared memory (or at least visible to other threads). When I was reading on fences, I came under impression that a release fence needs a matching acquire fence in the code reading the data commited by the other thread. I accepted it as it coincided with an old adage that no synchronisation will work if it happens only at one site (vide the famous 'double-check locking/singleton pattern is broken' warning). I know that Java's (and C++) memory model is more abstract than any contemporary physical architecture, so some aspects of it are irrelevant.
I freely admit that I have no experience with such low-level control as memory barriers, so I do not feel comfortable with using them. In particular, I can't see how writes being reordered after a release fence (which, as I understand, is still possible) is any different from writes from after the fence actually happening before it, the latter being forbidden. The fence location is irrelevant, what matters is the happens-before relationship. I would like to understand why the lone release fence is enough, or ''when'' it is enough, and when in that case an acquire fence will still be needed.
It looks like the
releaseFenceappeared inListcode as a result of these discussions: 1 and 2.To be fair with you, I didn't find there clear explanations why only
releaseFencewas added (without accompanyingacquireFence).So I don't know the real answer, but I have a version.
As I understand,
releaseFencealone could be enough.As I've read every CPU supported by Java preserves a so called load dependency.
It looks like load dependency was used to implement
finalJMM guarantees (see this article), so I guess, Scala might have used the same trick.Example of load dependency:
In
thread1(): releaseFence guarantees thato.a = 1andobj = oaren't reordered.In
thread2()Readvar r1 = o.ameans that we compute memory address for loading as[memory address of object o] + [offset of field a inside Obj].This is a load dependency: memory address for load
o.ais computed from memory address ofoloaded byo = obj.CPU preserving load dependency means that
o = objandvar r1 = o.aaren't reordered.Since in
thread1()o.a = 1andobj = oaren't reordered either, then readvar r1 = o.ainthread2()is guaranteed to see1.Note that this works because it's one object:
objchanges => we know thatthread1()is doneobjwe get load dependency => we are guaranteed to see all the writes fromthread1()which are beforeobj = oHere is an example when we need
acquireFence:Here there is no load dependency between the reads of
aandreadyinthread2().As a result
acquireFenceis required to prevent reordering of the reads ofaandready.Keep in mind that this is just a version.
As you correctly noted, this all is very low level. I don't know Java on such a deep level. (BTW as I understand fences are outside the JMM + JMM is defined only for Java (not Scala or JVM) - so I'm not sure that
happens-beforecan be applied here, at least as it's defined in the JMM)