Monday, March 23, 2009

 

Volatile Memory

With modern computers, volatile and non-volatile memory access becomes a somewhat tricky issue. A computer may have a number of processors or cores that cache and optimize memory reads and writes directly in the hardware itself. Likewise, the compiler and runtime may also do some amount of caching and optimization. As a result of all of this caching and optimization, it is quite possible that writes to physical memory actually occur in order that is different from the order in which those writes appear in source code. As you might imagine, this can cause problems if the programmer has not taken steps to synchronize access.

For example, imagine that the following code is running on Thread 1, where both variables are fields of a class.



// Thread 1
_result = 1;
_done = true;

 

Next, imagine that the following code is running on Thread 2, and that Thread 2 also has access to those same fields and is waiting for _done to go true.



// Thread 2
while (!_done)
{
Thread.Sleep(1000);
}
int result = _result;

 

What would you expect result to hold after the loop? Chances are, you would expect it to have a value of 1 because _result is set to 1 before _done is set to true. And, chances are you would be right some or most of the time. But, because of the caching and optimization mentioned above, it is actually possible that _done will physically be set to true before _result has physically be set to 1. Therefore, it is also possible that Thread 2 will read the old value of _result after Thread 2 sees the change in _done.

Potentially even worse, if Thread 1 continues to do some long running operation after setting _done, because of caching and optimization there is no guarantee that Thread 2 will even see the change to _done. Basically, for as long as Thread 1 continues to run, Thread 2 could be essentially deadlocked waiting for _done to change.

To get around this potential problem, .NET has the Thread.VolatileRead() and Thread.VolatileWrite() methods. The VolatileWrite() method causes the data to be written to a memory location immediatly instead of being cached. Similarly, the VolatileRead() method cause the memory location to be read directly instead of relying in a cached value.

If a field is shared between more than one concurrent thread and each of those threads always uses VolatileRead() and VolatileWrite() to access the field, the expected results will be achieved because the caching and optimziation issue will be avoided.

In addition to the Thread.VolatileRead() and Thread.VolatileWrite() methods that are part of the .NET framework, the C# language itself has a volatile keyword that can be used with fields. When volatile is used, all access to the field essentially goes through VolatileRead() and VolatileWrite() without explicitly having to use those methods.



private volatile bool _done;

 

And, the ECMA-334 spec (section 17.4.3) goes even further to say

A volatile read...is guaranteed to occur prior to any references to memory that occur after it in the instruction sequence.

and

A volatile write...is guaranteed to happen after any memory references prior to the write instruction in the instruction sequence.

It is important to note in those statements above that they are really talking about a reference to any field, not just the one that was marked volatile. Thus, in our trivial example, if _done was marked as volatile, then the memory for the _result field is also guaranteed to have been updated before the memory for the _done field is updated.



// Thread 1
_result = 1;
_done = true;

 

After all of this discussion, you will be delighted to know that you do not actually have to actually use the volatile keyword or Thread.VolatileRead() and Thread.VolatileWrite() method if you don't want to do so. Each of those techniques implements so-called "acquire semantics" and "release semantics". But, so does the standard lock that most people are familiar with. Therefore, many programmers may be more comfortable doing something like this.



private object _doneLock = new object();
private bool _done;

bool Done
{
get
{
lock(_doneLock )
{
return _done;
}
}
set
{
lock(_doneLock )
{
_done = value;
}
}
}



// Thread 1
_result = 1;
this.Done = true;


// Thread 2
while (!this.Done)
{
Thread.Sleep(1000);
}
int result = _result;

 

Likewise, you can also use the thread-safe Interlocked class methods which also use the acquire and release semantics, but I still suspect most programmers will be happy to stick with the basic lock.

Comments: Post a Comment





<< Home

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]