Well written article, nice and to the point. Do recommend. Decades ago I declared myself too stupid to use shared memory with threading; I have learned to avoid this whenever possible, or abstract away the memory access under a safe layer as soon as possible. One of the greatest decisions of my career. Memory model semantics is one of the parts of systems programming that is generally poorly understood; I have had lo…
Making Sense of Acquire-Release Semantics
41–50 of 72 posts
Re: Making Sense of Acquire-Release Semantics
#42I find that naming convention much more intuitive than release/acquire, though release/acquire is more general.
Re: Making Sense of Acquire-Release Semantics
#43I’ve read so much through the years on this, and I feel like it’s our Emperor’s Clothes - people pretend to understand, but does anyone actually understand this magic? Like not theoretically-superficially but in practice, or am I just too dumb to see the King’s new attire?
Re: Making Sense of Acquire-Release Semantics
#44No matter how many times I read about these, I'm always left just slightly confused. Acquire/release is about the ordering of instructions within a thread of execution. So if I have a writer writing X, then writing Y, then I need write-release to make sure that the compiler actually puts the instructions for Y after the instructions for X in the machine code. If I want to guarantee that the results of those writes ar…
> If I were writing a program in C99, I would assume it would still be possible to communicate the same intent / restrictions to the compiler, but I'm not sure because I haven't been able to find any resources that discuss doing so You cannot. See boehm, 'threads cannot be implemented as a library' ( https://web.archive.org/web/20240118063106if_/https://www.hp... ). You can do that in c11, however, which includes fun…
Re: Making Sense of Acquire-Release Semantics
#45Earlier quoted context omitted.
> If I were writing a program in C99, I would assume it would still be possible to communicate the same intent / restrictions to the compiler, but I'm not sure because I haven't been able to find any resources that discuss doing so You cannot. See boehm, 'threads cannot be implemented as a library' ( https://web.archive.org/web/20240118063106if_/https://www.hp... ). You can do that in c11, however, which includes fun…
And yet people did threaded programming in C before C11. Granted, you cannot do it in plain C99 -- in practice extensions were used. The hard part wasn't getting the fence instructions (asm will do it) but getting the compiler to not re-order things around fences, and `asm volatile ("" ::: "memory")` (or similar) would do that.
Re: Making Sense of Acquire-Release Semantics
#46I may be completely wrong, it's a complicated subject, but I think the wording here is potentially misleading.
As I understand it (and I may be wrong!), a full fence does the following : "all reads and all writes prior to the fence must occur before any reads or writes after the fence".
What I'm concerned about is people thinking this is a blocking behaviour, i.e. when we hit the fence, then at that point all prior reads and writes occur.
This is not the case.
The reads and writes prior to the fence can occur at any time - they could occur LONG AFTER we pass the fence - but what we do get from the fence is that the reads and writes prior to the fence WILL occur BEFORE any reads or writes after the fence.
So, to put it another way, the code is executing, we come to the fence - and absolutely nothing happens. The processor just keeps going. No reads occur, no writes occur.
Then at some point later on, both in time and in code, the process comes to another (say) read. NOW, finally, the processor MUST complete all reads and writes which were issued prior to the fence.
Re: Making Sense of Acquire-Release Semantics
#47Well written article, nice and to the point. Do recommend. Decades ago I declared myself too stupid to use shared memory with threading; I have learned to avoid this whenever possible, or abstract away the memory access under a safe layer as soon as possible. One of the greatest decisions of my career. Memory model semantics is one of the parts of systems programming that is generally poorly understood; I have had lo…
Re: Making Sense of Acquire-Release Semantics
#48Earlier quoted context omitted.
All CPUs commit in order and except precisely, because most other options are insane, or would drive you to it. However: single thread commit order =/= observability order. Observability order of memory operations --- which are the only operations that matter --- are governed by the memory consistency model of the architecture. x86 has what's generally referred to as strong ordering on memory operations. On x86, part…
Valid points, although I have another perspective on this bit: > But in the end: yes the reordering done by the CPU is the issue I think from a programmer perspective, the CPU side of things is mostly beside the point (unless you're writing assembly), and this contributes to the misunderstanding and air of mystery surrounding thread safety. At the end of the day the CPU can do anything, really. I'd argue this doesn't…
Re: Making Sense of Acquire-Release Semantics
#49> See how we added a new fence() call to put()? That fixes the reordering problem we’ve described at length. Now if the CPU gets bored waiting for entries[i] to be read into the cache, and tries to pull up the tail++ line so it happens sooner, bonk!, the tail++ line hits that fence and stops moving. We’ve forced the entry to be written before the tail is bumped. Problem solved! I may be completely wrong, it's a compl…
However, "completed" is misleading.
The write will still not be seen by readers.
"Complete" really means "the writer has done all the work he can do, which is necessary but insufficient".
For a reader to see the write, the readers must issue a read memory barrier before reading the value.
All of this is due to store buffers and cache invalidation requests.
In short, in principle, a write is known about only by the processor it occurred upon; if there are any other processors in the system, then unless special magic is performed (fences and so on), any writes they see from other processors are seen purely by chance and can happen in any order (including going backwards in time), or not happen at all.
I once read an article which framed all this in terms of source control.
Memory is like SVN, or Git.
You make local changes (on your processor, which has its local copy of memory). You then commit (write fence/atomic operation). No one else can see your changes until they update from source control and get the latest version (read fence).
Re: Making Sense of Acquire-Release Semantics
#50> See how we added a new fence() call to put()? That fixes the reordering problem we’ve described at length. Now if the CPU gets bored waiting for entries[i] to be read into the cache, and tries to pull up the tail++ line so it happens sooner, bonk!, the tail++ line hits that fence and stops moving. We’ve forced the entry to be written before the tail is bumped. Problem solved! I may be completely wrong, it's a compl…
The first is that a cpu core is a massively distributed system, and there is no single point in time when an operation is executed.
The second is that cpus will absolutely physically do reads out of order even when those reads logically have to happen in order. Suppose you read X, then fence, then read Y; the cpu may read Y immediately, immediately begin speculatively executing dataflows depending on Y, then later on read X. But then if, between the time when it read Y and it read X, Y changed, then it will roll back any intermediate computation dependent on Y and try again. But if Y didn't change between the time when it was initially read and the time when X was read, then it's semantically the same as if Y was read after X, so there is no problem.