使用双重检查锁定的内存读取可见性排序与写入顺序

Not*_*ull 6 .net c#

我有以下函数,旨在"memoize"无参数函数.意思是只调用一次函数,然后在其他时间返回相同的结果.

private static Func<T> Memoize<T>(Func<T> func)
{
    var lockObject = new object();
    var value = default(T);
    var inited = false;

    return () => {
        if (inited)
            return value;

        lock (lockObject) {
            if (!inited) {
                value = func();
                inited = true;
            }
        }

        return value;
    };
}
Run Code Online (Sandbox Code Playgroud)

我可以确定如果一个线程在锁外读取"inited == true",那么它将读取在"inited"设置为true之前写入的"值"吗?

注意:.NET中的双重检查锁定涵盖了它应该工作的事实,这个问题主要是检查我的实现是否正确并且可能获得更好的替代方案.

Cam*_*ron 5

不,因为inited不是volatile.volatile为您提供内存释放并获取您需要的围栏,以便建立正确的先发生关系.

如果之前没有释放围栏inited设置为true,那么在value另一个线程读取inited并将其视为true 时可能无法完全写入,这可能导致返回半构造对象.类似地,如果inited在第一次检查中读取之前有一个释放围栏但没有相应的获取围栏,那么该对象可能是完全构造的,但是看到inited为真的CPU核心还没有看到value被写入的记忆效应(缓存)一致性并不一定要求在其他核心上按顺序看到连续写入的影响.这将再次潜在地导致返回半构造的对象.

顺便说一句,这是已经非常详细记录的双重检查锁定模式的实例.

而不是使用捕捉局部变量(这将使编译器生成一个隐含的类来保存在非易失性领域的封闭了变量)拉姆达的,我建议明确将其创建自己的类volatile申请value.

private class Memoized<T>
{
    public T value;
    public volatile bool inited;
}

private static Func<T> Memoize<T>(Func<T> func)
{
    var memoized = new Memoized<T>();

    return () => {
        if (memoized.inited)
            return memoized.value;

        lock (memoized) {
            if (!memoized.inited) {
                memoized.value = func();
                memoized.inited = true;
            }
        }

        return memoized.value;
    };
}
Run Code Online (Sandbox Code Playgroud)

当然,正如其他人提到的那样Lazy<T>存在于此目的.使用它而不是自己动手,但了解事物如何运作的理论总是一个好主意.