I would like to create a class with two methods:
void SetValue(T value)
stores a value, but only allows storing a single value (otherwise it throws an exception).T GetValue()
retrieves the value (and throws an exception if there is no value yet).
I have the following desires/constraints:
- Reading the value should be cheap.
- Writing the value can be (moderately) costly.
GetValue()
should throw the exception only if the up-to-date value is absent (null
): It should not throw an exception based on a stalenull
value after a call toSetValue()
in another thread.- The value is written only once. This means
GetValue()
does not need to freshen the value if it is not null. - If a full memory barrier can be avoided, then it is (much) better.
- I get that lock-free concurrency is better, but I'm not sure if it is the case here.
I came up with several ways to achieve that, but I'm not sure which are correct, which are efficient, why they are (in)correct and (in)efficient, and if there is a better way to achieve what I want.
Method 1
- Using a non-volatile field
- Using
Interlocked.CompareExchange
to write to the field - Using
Interlocked.CompareExchange
to read from the field - This relies on the (possibly wrong) assumption that after doing an
Interlocked.CompareExchange(ref v, null, null)
on a field will result in the next accesses getting values that are at least as recent as those thatInterlocked.CompareExchange
saw.
The code:
public class SetOnce1<T> where T : class
{
private T _value = null;
public T GetValue() {
if (_value == null) {
// Maybe we got a stale value (from the cache or compiler optimization).
// Read an up-to-date value of that variable
Interlocked.CompareExchange<T>(ref _value, null, null);
// _value contains up-to-date data, because of the Interlocked.CompareExchange call above.
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
}
// _value contains up-to-date data here too.
return _value;
}
public T SetValue(T newValue) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
if (Interlocked.CompareExchange<T>(ref _value, newValue, null) != null) {
throw new System.Exception("Value already present.");
}
return newValue;
}
}
Method 2
- Using a
volatile
field - Using Ìnterlocked.CompareExchange
to write the value (with [Joe Duffy](http://www.bluebytesoftware.com/blog/PermaLink,guid,c36d1633-50ab-4462-993e-f1902f8938cc.aspx)'s
#pragmato avoid the compiler warning on passing a volatile value by
ref`). - Reading the value directly, since the field is
volatile
The code:
public class SetOnce2<T> where T : class
{
private volatile T _value = null;
public T GetValue() {
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
public T SetValue(T newValue) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
#pragma warning disable 0420
T oldValue = Interlocked.CompareExchange<T>(ref _value, newValue, null);
#pragma warning restore 0420
if (oldValue != null) {
throw new System.Exception("Value already present.");
}
return newValue;
}
}
Method 3
- Using a non-volatile field.
- Using a lock when writing.
- Using a lock when reading if we read null (we will get a coherent value outside the lock since references are read atomically).
The code:
public class SetOnce3<T> where T : class
{
private T _value = null;
public T GetValue() {
if (_value == null) {
// Maybe we got a stale value (from the cache or compiler optimization).
lock (this) {
// Read an up-to-date value of that variable
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
}
return _value;
}
public T SetValue(T newValue) {
lock (this) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
if (_value != null) {
throw new System.Exception("Value already present.");
}
_value = newValue;
return newValue;
}
}
}
Method 4
- Using a volatile field
- Writing the value using a lock.
- Reading the value directly, since the field is volatile (we will get a coherent value even if we don't use a lock since references are read atomically).
The code:
public class SetOnce4<T> where T : class
{
private volatile T _value = null;
public T GetValue() {
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
public T SetValue(T newValue) {
lock (this) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
if (_value != null) {
throw new System.Exception("Value already present.");
}
_value = newValue;
return newValue;
}
}
}
Other methods
I could also use Thread.VolatileRead()
to read the value, in combination with any of the writing techniques.
ReaderWriterLock(Slim)
? Are you sure that a simplelock
does not satisfy your needs? Why not? What is the usage pattern for theSetOnce
class? – TraceeReaderWriterLock
, I'll give it a lock. But just like the regularlock
, I think it will introduce a synchronization overhead at each read, which I'd rather avoid since the value is only changed once, so when reading a non-null value (without locks), one has the guarantee that it is not stale. – Emelda