Search code examples
c#.netmultithreadingconcurrencyparallel-processing

What is the logic behind Volatile.Read and Volatile.Write?


From MSDN, Volatile.Read():

Reads the value of a field. On systems that require it, inserts a memory barrier that prevents the processor from reordering memory operations as follows: If a read or write appears after this method in the code, the processor cannot move it before this method.

and Volatile.Write():

Writes a value to a field. On systems that require it, inserts a memory barrier that prevents the processor from reordering memory operations as follows: If a read or write appears before this method in the code, the processor cannot move it after this method.

I think I can understand the using scenarios of Volatile.Read() and Volatile.Write(), and have seen many examples explaining why these two methods help ensuring the correctness of program.

But I still wonder, what is the logic behind these rules?

Take Volatile.Read() as example, why it requires operations after it cannot be moved before it, but does not require anything from operations before it?

And also why it's opposite to Volatile.Write()?

Thank you!


Solution

  • The guarantees around volatile read and volatile write ensure that if one thread uses a volatile write to indicate that something is done, and then another thread uses a volatile read to notice that that something is done, then the second thread will see the full effects of that something.

    For instance, lets say that Thread1 initializes object A, and than does a volatile write to a flag indicating that it's done. All of the memory operations involved in initializing the fields of object A occur before the flag setting in the code. The guarantee is that these "cannot be moved after the volatile write" to flag, so by the time the flag is set in memory, the whole initialized object is in memory where other threads can see it.

    Now lets says that Thread2 is waiting for that object. It has a volatile read that sees flag get set, and then reads the fields of A and makes decisions based on what it has read. Those read operations occur after the volatile read in the code, and the volatile read guarantee ensures that they will occur after the volatile read in memory, so that Thread2 is guaranteed to see the fully initialized fields of object A, and not anything that existed before it.

    So: The writes that Thread1 does go out to memory before the volatile write to flag, which obviously must go out to memory before Thread2 can volatile read it, and the following reads in Thread2 happen after that so it sees the properly initialized object.

    That's why writes can't be delayed past volatile writes, and reads can't be moved up before volatile reads. What about vice versa?

    Well, lets say that Thread2, after it sees that A is initialized, does some work and writes it to some memory that Thread1 is using to decide how to initialize A. Those writes are guaranteed not to happen in memory until after Thread2 sees that A is done, and the reads that Thread1 makes to those locations are guaranteed to happen before the flag is set in memory, so Thread2's writes are guaranteed not to interfere with the initialization work.