6

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

  • 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#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;
    }
}

方法三

  • 使用非易失性字段。
  • 写入时使用锁。
  • 如果我们读取 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()结合任何写作技巧来读取值。

4

2 回答 2

1

好吧,不确定波动性,但如果你不介意一点滥用和调用第二种方法......(它也不依赖于可空性;可自由用于值类型)也避免在 getter 中进行空检查。仅在写入时完成锁定,因此 AFAIK,唯一的负面影响来自在获取值时调用委托。

public class SetOnce<T>
{
    private static readonly Func<T> NoValueSetError = () => { throw new Exception("Value not yet present.");};

    private Func<T> ValueGetter = NoValueSetError;
    private readonly object SetterLock = new object();

    public T SetValue(T newValue)
    {
        lock (SetterLock)
        {
            if (ValueGetter != NoValueSetError)
                throw new Exception("Value already present.");
            else
                ValueGetter = () => newValue;
        }

        return newValue;
    }

    public T GetValue()
    {
        return ValueGetter();
    }
}

事实上,我对此感到非常愚蠢,并感到有点辱骂。我很想看到有关这样做的潜在问题的评论。:)

编辑:刚刚意识到这意味着第一次调用SetValue(null)意味着“null”将被视为有效值,并将无异常返回 null。不确定这是否是您想要的(我不明白为什么null不能是有效值,但如果您想避免它,只需在 setter 中进行检查;在 getter 中不需要)

EDITx2:如果您仍想将其限制为class并避免null使用值,则简单的更改可能是:

public class SetOnce<T> where T : class
{
    private static readonly Func<T> NoValueSetError = () => { throw new Exception("Value not yet present.");};

    private Func<T> ValueGetter = NoValueSetError;
    private readonly object SetterLock = new object();

    public T SetValue(T newValue)
    {
        if (newValue == null)
            throw new ArgumentNullException("newValue");

        lock (SetterLock)
        {
            if (ValueGetter != NoValueSetError)
                throw new Exception("Value already present.");
            else
                ValueGetter = () => newValue;
        }

        return newValue;
    }

    public T GetValue()
    {
        return ValueGetter();
    }
}
于 2013-02-21T15:13:02.400 回答
0

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

看:

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

这段代码不是原子的,也不是线程安全的,如果不是 inside lock。其他线程仍然可能设置_valuenullbetweenifreturn。您可以做的是设置为局部变量:

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

但它仍然可以返回停顿值。您最好使用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;
    }
}
于 2013-02-21T14:49:46.373 回答