Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 296
Lifu Huang Avatar asked Jan 11 '17 03:01

Lifu Huang


3 Answers

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.

like image 99
Matt Timmermans Avatar answered Oct 13 '22 11:10

Matt Timmermans


The logic behind these rules is called Memory Model.
In .NET we have quite weak memory model (see ECMA-335), which means that compiler, jit and cpu are allowed to do a lot of optimizations (as long as they keep single threaded semantics and volatile semantics) and it's really awesome in terms of possibilities for optimizations.
It's allowed for compiler/jit/cpu to make any optimizations as long as they satisfy the following:

Conforming implementations of the CLI are free to execute programs using any technology that guarantees, within a single thread of execution, that side-effects and exceptions generated by a thread are visible in the order specified by the CIL. For this purpose only volatile operations (including volatile reads) constitute visible side-effects. (Note that while only volatile operations constitute visible side-effects, volatile operations also affect the visibility of non-volatile references.)

Which means that all your code is assumed to be single-threaded unless you use implicit or explicit volatile operations.
For example,

Acquiring a lock ( System.Threading.Monitor.Enter or entering a synchronized method) shall implicitly perform a volatile read operation, and releasing a lock ( System.Threading.Monitor.Exit or leaving a synchronized method) shall implicitly perform a volatile write operation.

Which means that it's not possible to move any operations (from lock statement) above (implicit Volatile.Read prevents this) and it's not possible to move them below lock (implicit Volatile.Write prevents this). So they stay right inside the lock statement, but it's still possible for them to be reordered or optimized inside this lock statement.

like image 21
Valery Petrov Avatar answered Oct 13 '22 11:10

Valery Petrov


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

Described by Matt Timmermans.

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?

Let see what is possible according to volatile semantics.

RULES

Volatile.Read                      ┇       Volatile.Write
⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩                     ┇       ⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩
** later cannot move here  **      ┇       x = writeSomething      
°MemoryBarier°                     ┇       °MemoryBarier°  
x = readSomething                  ┇       ** sooner cannot move here **
〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰

EXECUTION ORDER (2 OPTIONS)

W-R                                        R-W 
⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩                             ⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩
Write (sooner)                     ┇       Read  (sooner)
Read  (later)                      ┇       Write (later)
〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰

REORDER
                                    
W-R                                        R-W 
⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩                             ⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩⬩
** later cannot move here  **      ┇       Not possible
°MemoryBarier°      ¦ (later)      ┇
x = readSomething   ¦ (later)      ┇
x = writeSomething  ¦ (sooner)     ┇ 
°MemoryBarier°      ¦ (sooner)     ┇
** sooner cannot move here **      ┇

You can see that rules of volatile read and write can be slightly counterintuitive.

Have look at the table of “before” possibilities

Read           |  Write          |  Volatile.Write  |  Volatile.Read
Volatile.Read  |  Volatile.Read  |  Volatile.Read   |  Volatile.Read
---------------|-----------------|------------------|-------------------
No harm when   | Can be          | Can be reordered.| Cannot be reordered.
reordered.     | reordered.      | Can be a harm.   |
One should use | Missing MT tech.|                  |
some MT        | Can be a harm.  |                  |
technics.      |                 |                  |            

So any reasonable before instructions are already governed by rules of volatility.

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

Each is complementary to another. So one can read and write in a volatile manner as needed.

like image 28
Yarl Avatar answered Oct 13 '22 11:10

Yarl