我想用两种方法创建一个类:
void SetValue(T value)
存储一个值,但只允许存储一个值(否则它会引发异常)。T GetValue()
检索值(如果还没有值则抛出异常)。
我有以下愿望/限制:
- 读取值应该很便宜。
- 写入值可能(适度)昂贵。
GetValue()
仅当最新值不存在时才应抛出异常 (null
):它不应null
在调用SetValue()
另一个线程后基于陈旧值抛出异常。- 该值只写入一次。这意味着
GetValue()
如果它不为空,则不需要更新该值。 - 如果可以避免完整的内存屏障,那么它(好多)更好。
- 我知道无锁并发更好,但我不确定这里是否是这种情况。
我想出了几种方法来实现这一点,但我不确定哪些是正确的,哪些是有效的,为什么它们是(不)正确和(低)效率的,以及是否有更好的方法来实现我想要的。
方法一
- 使用非易失性字段
- 用于
Interlocked.CompareExchange
写入字段 Interlocked.CompareExchange
用于从字段中读取- 这依赖于(可能是错误的)假设,即在
Interlocked.CompareExchange(ref v, null, null)
对字段执行操作后将导致下一次访问获得的值至少与所Interlocked.CompareExchange
看到的值一样新。
编码:
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;
}
}
方法二
- 使用
volatile
字段 - 使用 Ì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`)。 - 直接读取值,因为字段是
volatile
编码:
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;
}
}
方法三
- 使用非易失性字段。
- 写入时使用锁。
- 如果我们读取 null,则在读取时使用锁(因为引用是原子读取的,我们将在锁外获得一致的值)。
编码:
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;
}
}
}
方法四
- 使用 volatile 字段
- 使用锁写入值。
- 直接读取值,因为该字段是易失的(即使我们不使用锁,我们也会得到一个一致的值,因为引用是原子读取的)。
编码:
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;
}
}
}
其他方法
我还可以Thread.VolatileRead()
结合任何写作技巧来读取值。