This slide explains how coherence can be an issue even on systems with only one CPU.
The root of the problem is that caches are not part of the memory abstraction that parts external to the CPU see. They are an implementation detail that the CPU uses in providing this abstraction. If there is a device that accesses or modifies memory independently of the CPU, such as a network card sending or receiving traffic, the CPU cannot know which of it's cache lines accurately reflect main memory. Likewise, the memory card cannot know that main memory might not accurately reflect the most-updated cache line for that address.
One common solution is to provide mechanisms for marking regions of memory as non-cacheable, thus forcing the transactions to go all the way to main memory every time.
The coherence problem seems like we have a source in memory being duplicated and distributed to elsewhere and manipulated. It make me think of the github things, that one source code can be forked by people and need version control.
One example I can think of is when reading from memory mapped ports (memory locations tied to IO ports, so the value of the memory can change asynchronously), using the keyword volatile, marks the memory location as not-cachable. The keyword volatile helps tell the compiler to instead retrieve the data from the memory location each time
@enuitt The semantics of the volatile keyword are actually a little different than that. The volatile keyword indicates to the compiler that the variable in question may be modified from a different context (e.g. it may be modified by hardware). Hence, the why it is called "volatile", because the memory value is not "stable", and may be modified by another entity.
In terms of implementation, this indicates to the compiler that it cannot optimize away any memory accesses to that variable, and that the variable must be stored somewhere in memory. Thus, every memory read or write to the variable in the code must correspond to an actual read/write instruction. This is key, because the compiler may chose to update the variable in a register several times, and then write it back once done.
However, this does not mark the memory location as non-cacheable. Instead, it simply indicates that value should always be fetched from/stored to memory. In some sense, you can consider the registers as a cache of the value, but in terms of the actual processor cache, the volatile keyword has no affect on this. Whether or not pages are cached is actually controlled by the virtual memory system, so it is the OS that decides which memory pages get cached.
The volatile keyword will guarantee that memory location's value is always fetched, and never stored temporarily in a register, but if the system is not cache-coherent, then it will not guarantee that non-coherent updates are seen.