Ian Lance Tailor started a
discussion that has in the meantime reached
Planet Debian via
Miry and
Giacomo.
The single-threaded memory model is the only sane choice for languages that are as close to the "metal" as C and C++ are. For a multi-threaded model to work, the compiler needs to have intrinsic knowledge of "locking", and it needs to emit the appropriate code around all accesses to variables that cannot be accessed atomically (which also means that signal handling does not work — the signal might arrive in a critical section).
Java uses such a model. In the language, there is the "synchronized" keyword, which can be used either as a modifier on a method definition (locking the object the method is called on), or before a block (locking an explicitly specified object). The lock is recursive, so calling another "synchronized" method on the same object works fine, leaving the last one finally unlocks the object. Memory consistency is achieved by flushing the object before unlocking (so accessing an object that another thread may use without locking it is still undefined behaviour — but class authors can avoid that by allowing access only through "synchronized" methods).
I doubt we want the same in C/C++.
For one, it adds a recursive lock to each instance of a class or struct, whether needed or not, since the compiler cannot rule out that some code might want to use this object as a lock. Also, the object inclusion and inheritance rules are quite different, as Java does not have inclusion, and only allows simple inheritance. Basically, if I were to emulate Java's rules, I'd need a "virtual" base class to provide the lock, which is a lot of extra overhead since every derived class now needs to keep the offset to the most derived class.
The other two big show stoppers are the "intrinsic" knowledge of locking that the compiler must have (it needs to know how to embed a recursive lock into an object, which is an operating-system dependent structure as it stores some thread identifier for the current holder of the lock) and that it becomes a lot more difficult to write signal handlers, as you need to avoid any code that might implicitly lock some object (which is true already, but there is no implicit locking). Java can do that, as they have a virtual machine that provides locking primitives and object lookup (so they can keep the lock separate).
There is a change to the memory model I'd like to see though: the concept of an I/O buffer that behaves like regular memory (so load/store coalescing/reordering is allowed), but will be flushed before volatile accesses and re-read afterwards (optionally, with finer grained control). This would work for both hardware accesses (where the volatile access triggers some hardware) and for "hosted" environments (where the compiler assumes that system calls perform volatile accesses), and still allow most optimisations like writing in the hardware's natural bus width.
If you want to write reliable multi-threaded code with current compilers, there are two simple rules:
- any data structure shared by multiple threads needs to be marked volatile
- any access to a shared data structure needs to be guarded by a lock
The accesses to the shared data structure are ordered with regard to the locking functions and cannot be optimized out, which is exactly what you want. The appropriate mental model is that anything that is not declared volatile (or handled by a platform locking primitive) cannot be seen by the other threads.