从Interlocked变量中读取最新值,只对变量进行一次写入

Suz*_*ron 6 c# volatile interlocked lock-free

我想用两种方法创建一个类:

  • void SetValue(T value) 存储一个值,但只允许存储单个值(否则会抛出异常).
  • T GetValue() 检索值(如果还没有值,则抛出异常).

我有以下愿望/约束:

  • 读取价值应该便宜.
  • 写入价值可能(适度)成本高昂.
  • GetValue()只有在最新值不存在时才抛出异常(null):null在调用SetValue()另一个线程后,它不应该基于陈旧值抛出异常.
  • 该值仅写入一次.GetValue()如果值不为null,则表示不需要刷新值.
  • 如果可以避免完全的内存屏障,那么(更好).
  • 我得到的无锁并发性更好,但我不确定这是否是这种情况.

我提出了几种实现这一目标的方法,但我不确定哪些是正确的,哪些是有效的,为什么它们(正确)和(有效),以及是否有更好的方法来实现我想要的东西.

方法1

  • 使用非易失性字段
  • 使用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

方法2

  • 使用volatile字段
  • 使用Ìnterlocked.CompareExchange to write the value (with [Joe Duffy](http://www.bluebytesoftware.com/blog/PermaLink,guid,c36d1633-50ab-4462-993e-f1902f8938cc.aspx)'s#pragma to avoid the compiler warning on passing a volatile value byref`).
  • 直接读取值,因为该字段是 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

方法3

  • 使用非易失性字段.
  • 写作时使用锁定.
  • 如果我们读取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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

方法4

  • 使用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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

其他方法

我还可以使用Thread.VolatileRead()读取值,结合任何写入技术.

And*_*rey 0

除了 (#3) 的方法之外,您的任何方法都lock无法正常工作。

看:

    if (_value == null) {
        throw new System.Exception("Value not yet present.");
    }
    return _value;
Run Code Online (Sandbox Code Playgroud)

这段代码不是原子的,也不是线程安全的,如果不在lock. 其他线程仍然有可能设置_value为和null之间。你可以做的是设置为局部变量:ifreturn

    var localValue = _value;
    if (localValue == null) {
        throw new System.Exception("Value not yet present.");
    }
    return localValue;
Run Code Online (Sandbox Code Playgroud)

但它仍然可以返回摊位价值。你最好使用lock——清晰、简单、快速。

编辑:避免使用lock(this),因为this在外部可见,第三方代码可能会决定lock在您的对象上使用。

编辑2:如果永远不能设置空值,那么只需执行以下操作:

public T GetValue() {
    if (_value == null) {
        throw new System.Exception("Value not yet present.");
    }
    return _value;
}

public T SetValue(T newValue) {
    lock (writeLock)
    {        
        if (newValue == null) {
            throw new System.ArgumentNullException();
        }
        _value = newValue;
        return newValue;
    }
}
Run Code Online (Sandbox Code Playgroud)