6 ms·
> Before then, the mechanism for lock-free programming patterns was the volatile qualifier and various nonstandard barrier intrinsics. Ehhhhhh… > The semantic
by klodolph 7y ago
> Before then, the mechanism for lock-free programming patterns was the volatile qualifier and various nonstandard barrier intrinsics.
Ehhhhhh…
> The semantics of volatile were never clearly defined…
Well, the problem is that volatile was defined, more or less. It means that you get load and store instructions in machine code that correspond to the appearance of loads and stores in the source code. Importantly, this does not imply anything about the memory model on multiprocessor systems.
So "volatile" was never supposed to be used for threading. What you can use it for is:
- Memory-mapped I/O
- Values that are changed in interrupts
- Values changed on other threads on x86 if you just don’t give a damn
- longjmp/setjmp
- Values changed on other threads on Itanium
What happens when you don’t use “volatile” is that the compiler will do dead store elimination and optimize out loads. For example,
// Global;
int x = 0;
void wait_for_x(void) { while (x == 0) {} }
If `x` is modified by an interrupt, it should be marked `volatile` because otherwise the compiler will helpfully optimize out the test in the loop.
What people actually did for lock-free programming in the pre-C++-11 days was to use those non-portable intrinsics or inline assembler. If someone used “volatile”, it was usually out of ignorance.
- raphlinus 7y agoOk, I want to get this right. The major point I'm trying to make is that without the Vulkan memory model, the way to get atomic loads and stores is to add the "volatile" memory qualifier to the buffer interface block. This has basically the same set of problems as using "volatile" in pre-C++11 code. There is definitely precedent for using "volatile" in multithreaded code (though, researching this, I think there may be stronger precedents in C than in C++, see [1], particularly the "In C++11, normally never use volatile for threading, only for MMIO" answer). I agree a good argument can be made that it's incorrect. To be clear, the argument that volatile is incorrect for multithreading is extremely strong from the perspective of standard C and C++, but I think in practice it was considered quite usable. I have a feeling we agree on the substance (a memory model is a dramatic improvement over using volatile), but I can probably improve the wording. [1]: https://stackoverflow.com/questions/4557979/when-to-use-volatile-with-multi-threading https://stackoverflow.com/questions/4557979/when-to-use-vola... ETA: Here's what I consider a great reference for the widespread use of volatile for multithreaded programming. It's from 2001, but by Andrei Alexandrescu, who I think can be considered an authority: https://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766 https://www.drdobbs.com/cpp/volatile-the-multithreaded-progr...
- raphlinus 7y agoFollowup: I've changed the language a bit, I hope you agree it's an improvement. https://github.com/raphlinus/raphlinus.github.io/commit/6b92d19492f8052776d84894dda000e5376d1ff1 https://github.com/raphlinus/raphlinus.github.io/commit/6b92...
- ncmncm 7y agoMany people have written code targeting only MSVC, where volatile is documented to have atomic-like properties, and targeting only x86, where the bus is especially forgiving, albeit at massive costs in extra logic to make it so. So it's not so much ignorance at work as (pathological) tolerance of MS and x86 lock-in. That said, I know a FreeBSD kernel committer who in 2014 was convinced that "volatile" sufficed for thread synchronization on Gcc and any arch. So there is also plenty of ignorance at work. You could say that MS, Intel, and AMD have benefitted from such ignorance through the large amount of de-facto unportable code it engendered.
- fluffything 7y ago> Memory-mapped I/O If you are using volatile to interact with memory-mapped I/O, that means that there is a _thread of execution_ somewhere also interacting with that memory - otherwise why are you reading/writing to it? - such that if those reads and writes are not synchronized the program has a data-race and the behavior is undefined. The claim that this is not a data-race because those threads of execution are "outside my process" is incorrect: the C standard doesn't know about processes. Whether one thread runs in a CPU and writes to a memory mapped DMA region, and the other thread is in your network card reading from that same memory region doesn't matter at all. The right tool here is a `volatile atomic` load / store. > - Values changed on other threads on x86 if you just don’t give a damn If by "don't give a damn" you mean "don't care about UB" then this is correct, because doing that is UB, and UB can do anything. > Values that are changed in interrupts From the C standard point-of-view, an interrupt handler suspends a thread of execution, and the handler runs in a different thread of execution. Whether these threads are serialized by the OS or not doesn't matter, reading/writing across threads without synchronization is UB. In particular for interrupt handlers, using volatile to modify memory from an interrupt handler doesn't imply that the thread reading that memory will see the modification (e.g. the compiler might have cached that in a register). Using volatile to read the memory isn't sufficient either, because volatile is not a compiler fence, and it doesn't synchronize-with anything, not even volatile. The right tool here is a `volatile atomic` load / store. > - longjmp/setjmp The C standard requires the usage of volatile here, otherwise the behavior is undefined. There is only one thread of execution at play here, so no synchronization is necessary. Similar control-flow constructs like throwing and catching exceptions do not require the use of volatile here, so the reason one must use volatile here is because longjmp/setjmp is a poorly designed language feature.