C#中原始Lazy的问题在于,如果要引用this,则必须将初始化放入构造函数中。对于我来说,这是95%的情况。这意味着我必须将关于惰性属性的一半逻辑放入构造函数中,并具有更多样板代码。

请参阅以下问题:https://stackoverflow.com/questions/53271662/concise-在c-sharp / 53316998#53316998中转为懒惰加载属性的方法现在,我制作了一个新版本,实际上将初始化部分移到了“获取”时刻。这样可以节省样板代码,并且我可以将与此属性有关的所有内容组合在一起。

但是:


当前的实现是“安全的”吗?
与原始性能相比,还有任何重要的性能考量吗?
在高流量应用程序中我还缺少其他我需要关注的东西吗?

类:

public class MyLazy
{
    private object  _Value;
    private bool    _Loaded;
    private object  _Lock = new object();


    public T Get<T>(Func<T> create)
    {
        if ( _Loaded )
            return (T)_Value;

        lock (_Lock)
        {
            if ( _Loaded ) // double checked lock
                return (T)_Value;

            _Value   = create();
            _Loaded = true;
        }

        return (T)_Value;
    } 


    public void Invalidate()
    {
        lock ( _Lock )
            _Loaded = false;
    }
}


使用:

MyLazy _SubEvents = new MyLazy();
public SubEvents SubEvents => _SubEvents.Get(() =>
{
    // [....]
    return x;
});


或类似:

MyLazy _SubEvents = new MyLazy();
public SubEvents SubEvents => _SubEvents.Get(LoadSubEvents);

public void LoadSubEvents()
{
    // [....]
    return x;
}


评论

@AdrianoRepetti这将是一个很好的答案;-)

@AdrianoRepetti,我实际上同意,所以我提供了第二个示例(我通常如何使用它)。问题是我确实有95%的时间需要访问实例变量,并放入我真正不喜欢的构造函数。因为如果您拥有5个以上的属性,那么实际上要将大量逻辑移入构造函数中,这意味着您实际上必须跳转到代码中,因为样板的其余部分实际上需要在构造函数之外。

Mod注意:请不要使用注释来引发有关一个问题以及如何编写正确的线程安全代码的扩展讨论。评论已清除。如果愿意,欢迎您继续在Code Review Chat中进行讨论:)

System.Layz 的源代码:referencesource.microsoft.com/#mscorlib/system/…(好奇的人的称呼)

嗨@OrkhanAlikhanov,谢谢!我尝试剥离大量该资源以尝试达到核心。如果您认为这很有趣,请查看我发布的答案之一中的结果。

#1 楼

将Lazy反向移植到.NET 2.0之后,我想提供一个变体...
首先,这看起来很像LazyInitializer。如果这对您有用,那么就无需创建Lazy<T>变体了。
话虽如此,让我们继续...

在进入代码之前,让我们看看一些可能出错的地方:

ABA问题

线程可见性/代码重新排序
双重初始化
值工厂递归
失败的初始化
防止垃圾收集

关于您问题的代码在可见性方面将失败。其他线程将不知道您更改了_Loaded。实际上,它可能成为优化参数_Loaded的牺牲品,因此每个方法调用只能读取一次。 create调用Get<T>将会有问题,它将覆盖_Value ...,这意味着_Value可以在初始化后更改。
此外,可以在调用_Loaded之前重新设置执行顺序以设置create

第二个版本:
volatile此处不够保证。当_Loaded易失性时,看不到对_Value的更改。因此,对于另一个线程,它可能以声称已加载但_Value看起来没有变化的状态到达对象。实际上,它可能似乎已部分更改(已读过)。
注意:鉴于我习惯于针对.NET的多个版本,我警告一下,我认为某些错误会导致其中的任何失败。包括.NET 2.0,.NET 3.0和.NET 3.5。其中,volatile语义已损坏。这让我在volatile周围格外谨慎。
我们仍然需要担心create调用Get
我很高兴看到T移至课程级别。

第三个版本:
初始化已移至构造函数。我认为这违背了目的,如果您可以在构造函数中使用它……为什么不使用Lazy<T>
使用Boxed解决了部分更改的问题。您可能在这里使用过StrongBox<T>
我看到您从参考来源继承了一些怪异...他们奇怪地检查Boxed的原因(也是他们不使用StrongBox<T>的原因)是因为它可能不包含值,但有一个缓存的异常。
在您以前的版本中,当create引发时,Get引发,另一个线程有机会初始化。但是,可以通过以下方式初始化Lazy<T>:存储异常并向尝试读取该异常的每个线程抛出该异常(请参阅LazyThreadSafetyMode)。
除此之外,使用Volatile修复了其他可见性和重新排序。问题。
注意:当factory抛出时,m_valueFactory已被设置为前哨。这意味着下一个要调用的线程将运行哨兵,从而将Value初始化为default(T)



可以简化为lock吗?

否。正如您已经猜到的那样。



工厂是否可以将布尔值和空值替换为前哨?

取决于您的目的想要(请参阅上面的注释),是的。由于LazyThreadSafetyMode,引用源具有它的方式。



如果您认为逻辑是好的,可以用一个内联new代替整个CreateValue()。 Boxed(factory())?

否。你可能有boxed = new Boxed(m_valueFactory());。但是,您仍然持有对m_valueFactory的引用,这可能会阻止垃圾回收。另外,如果递归,则为StackOverflowException。您可以将m_valueFactory设置为null...。但是,由于递归的可能性,可能会导致NullReferenceException。因此,我们需要将其设置为哨兵或将其设置为null并添加一个null检查。无论哪种方式,CreateValue都不是一个衬纸。



第四版:
注意对_value的写入,当它是值类型时,它们可能不是原子的。有一个很好的理由说明为什么以前的版本已装箱:另一个线程可能会看到部分初始化的值(撕裂读取)。在.NET Core中这是安全的,但在.NET Framework中则不是。
注意:此代码实际上在现代.NET中尤其是在.NET Core中可以很好地工作。
我将使用StrongBox<T>修复此问题。与firda的答案类似。
关于firda的答案……它保留在valueFactory上,在初始化后保持引用不变,使其指向的内容都保持活动状态,从而防止垃圾收集器将其引用。这是设计使然。可能不是您想要的。修改它的一种可能方法是在factory中的null的末尾将lock设置为GetOrCreate(当然,在返回之前)。
回到读取读取的问题,请考虑以下演示:
public struct Long8
{
    public long D1;
    public long D2;
    public long D3;
    public long D4;
    public long D5;
    public long D6;
    public long D7;
    public long D8;

    public long Val => D1 + D8;
}

public class Program
{
    public static void Main()
    {
        // We are using Lonh8 because writing to it would not be atomic
        // Here DataInit is poor man's lazy
        var dataInit = new DataInit<Long8>();
        // value will hold the values that the reader gets
        var value = default(Long8);
        // we use done to signal the writer to stop
        var done = 0;
        var reader = Task.Run
        (
            () =>
            {
                // We are reading the values set by the writer
                // We expect Val to be 1 because
                // the writer does not write something that would give a different value
                var spinWait = new SpinWait();
                do
                {
                    // This loop is here to wait for the writer to set a value since the last call to invalidate
                    while (!dataInit.TryGet(out value))
                    {
                        spinWait.SpinOnce();
                    }
                    // dataInit.Invalidate();
                    // Console.Write(".");
                    spinWait.SpinOnce();
                } while (value.Val == 1);
                // Console.WriteLine();
            }
        );
        var writer = Task.Run
        (
            () =>
            {
                // Here we will write { D1 = 1, D8 = 0 } and { D1 = 0, D8 = 1 }
                // In either case Val should be 1
                var spinWait = new SpinWait();
                while (Volatile.Read(ref done) == 0)
                {
                    dataInit.Init(new Long8 { D1 = 1, D8 = 0 });
                    spinWait.SpinOnce();
                    dataInit.Invalidate();
                    dataInit.Init(new Long8 { D1 = 0, D8 = 1 });
                    spinWait.SpinOnce();
                    dataInit.Invalidate();
                }
            }
        );
        reader.Wait();
        System.Diagnostics.Debugger.Break();
        Volatile.Write(ref done, 1);
        writer.Wait();
        Console.WriteLine(value.Val);
    }

    public class DataInit<T>
    {
        private T _data;
        private volatile bool _initialized;

        public void Init(T value)
        {
            // So, we write to a generic field
            // And then to a volatile bool to notify that we did write to the field
            // Yet, when T is a value type larger than the word size of the CPU
            // Another thread may find that _initialized = true but _data is not what we would expect
            _data = value;            // Write 1  <----------------
            _initialized = true;      // Write 2 (volatile) <------
        }

        public void Invalidate()
        {
            _initialized = false;
        }

        public bool TryGet(out T value)
        {
            // Here we are reading the volatile before we read the other field...
            // Does it make any difference?
            if (_initialized)
            {
                value = _data;
                if (_initialized)
                {
                    return true;
                }
            }
            value = default(T);
            return false;
        }
    }
}

此代码演示了阅读错误。在.NET Framework和.NET Core中。显示的结果是,在_data之间可以读取_initialised = false; _data = value; _initialised = true;,这是部分设置的。更具体地说:读取器读取该值已初始化并开始读取,同时另一个线程正在修改该值。只要您的代码没有Invalidate(firda在注释中提到),您的代码就不会遭受这种特殊的命运。
这是要小心的方法。
基于VisualMelon引入的一些修改,在C#中重现十进制的残缺读取,这仅是出于演示目的,这不是您在生产中想要的。
考虑valueFactory读取ValuevalueFactory抛出的情况。



我希望我们可以为对象更改它?保存单独的类。

是的,您可以使用object而不是LazyHelper



是否可以内联其他一些方法,例如ViaFactory是的,实际上,编译器可以进行此优化。



对我来说,Value第一次被递归调用感到有些奇怪,但这可能与我剥离的其他代码(即异常缓存)有关。

我我不确定为什么他们要在corefx中使用这种设计。但是-至少对于您的代码而言-与再次读取该字段相比,让ViaFactoryExecutionAndPublication返回是更好的设计。



不确定是否允许,但是如果我什至更多内联,您真的会得到一个“正常的”带有易失性的双重检查锁。主要区别在于,更衣室对象也被用作_loaded标志。

是的,可以。第一个线程将设置_state = null;,然后第二个线程进入lock,但是您不希望它再次调用_factory。因此,您检查它是否仍然是您设置的内容(不是,它是null),并且可以正确地决定不输入if




好了,现在我要写一个...
我将要满足的要求是:

是线程安全的
允许延迟初始化
无效
要在valueFactory上传递Get


我还将添加一个TryGet方法,用于那些希望在初始化但想要尝试获取值的线程初始化它。
首先我基于firda制作了一个:
public class MyLazy<T>
{
    private readonly object _syncroot = new object();
    private StrongBox<T> _value;

    public T Get(Func<T> valueFactory)
    {
        if (valueFactory == null)
        {
            throw new ArgumentNullException(nameof(valueFactory));
        }
        var box = Volatile.Read(ref _value);
        return box != null ? box.Value : GetOrCreate();

        T GetOrCreate()
        {
            lock (_syncroot)
            {
                box = Volatile.Read(ref _value);
                if (box != null)
                {
                    return box.Value;
                }
                box = new StrongBox<T>(valueFactory());
                Volatile.Write(ref _value, box);
                return box.Value;
            }
        }
    }

    public void Invalidate()
    {
        Volatile.Write(ref _value, null);
    }

    public bool TryGet(out T value)
    {
        var box = Volatile.Read(ref _value);
        if (box != null)
        {
            value = box.Value;
            return true;
        }
        value = default(T);
        return false;
    }
}

让我们清楚一点,firda的是一个很好的解决方案。这主要区别在于我没有将valueFactory传递给构造函数(我也使用StrongBox<T>,因为没有理由不使用提供的API)。正如您在“ 95%的情况”问题中提到的那样,您可以在构造函数中使用valueFactory,这适用于其余情况。
请注意,当调用Invalidate时,下一个要输入的线程将运行valueFactory
对于踢球,我做了一个没有显示器的游戏(lock)。此版本改用ManualResetEventSlim。这意味着它可以一次唤醒所有线程,而不会一次唤醒线程。对于并行运行许多线程的情况,这将是一件好事。但是,它需要额外的工作才能正常工作。对于初学者,如果valueFactory抛出异常,我们必须唤醒线程,让其中一个尝试初始化,然后让其余的再次等待……执行此操作直到它们全部失败或一个成功,这意味着我们需要循环。此外,使用lock可以自动为我们处理重入,使用ManualResetEventSlim可以自动处理。
此外,我建议您看看Interlocked.CompareExchange
public class Lazy<T>
{
    private readonly ManualResetEventSlim _event;
    private Thread _ownerThread;
    private int _status;
    private StrongBox<T> _value;

    public Lazy()
    {
        _event = new ManualResetEventSlim(false);
    }

    public T Get(Func<T> valueFactory)
    {
        // _status == 0 ::          Not Initialized
        // _status == 1 ::                              Initializing or Initialized
        // _ownerThread == null :: Not Initialized or                  Initialized
        // _ownerThread != null ::                     Initializing
        // _value == null ::        Not Initialized or  Initializing
        // _value != null ::                                            Initialized
        if (valueFactory == null)
        {
            throw new ArgumentNullException(nameof(valueFactory));
        }
        var ownerThread = Volatile.Read(ref _ownerThread);
        if (ownerThread == Thread.CurrentThread)
        {
            // We have reentry, this is the same thread that is initializing on the value
            // We could:
            // 1. return default(T);
            // 2. throw new LockRecursionException()
            // 3. let it in
            // We are doing that last one
            return Create();
        }
        // Note: It is ok to loop here. Since Threads will only ever change _ownerThread to themselves...
        // there is no chance that _ownerThread suddently changes to the current thread unexpectedly
        // Why we loop? See at the bottom of the loop.
        while (true)
        {
            ownerThread = Volatile.Read(ref _ownerThread);
            if (ownerThread == null)
            {
                // There is no thread initializing currently
                // Either the value has already been initialized or not
                // Check status to tell
                if (Volatile.Read(ref _status) == 1)
                {
                    // Value has already been initialized
                    var box = Volatile.Read(ref _value);
                    if (box == null)
                    {
                        // Another thread invalidated after we did read _status but before we got Value
                        continue;
                    }
                    return box.Value;
                }
                // Value is yet to be initialized
                // Try to get the right to initialize, Interlocked.CompareExchange only one thread gets in
                var found = Interlocked.CompareExchange(ref _status, 1, 0);
                if (found == 0)
                {
                    // We got the right to initialize
                    var result = default(T);
                    try
                    {
                        try
                        {
                            Volatile.Write(ref _ownerThread, Thread.CurrentThread);
                            result = Create();
                        }
                        finally
                        {
                            // Other threads could be waiting. We need to let them advance.
                            _event.Set();
                            // We want to make sure _ownerThread is null so another thread can enter
                            Volatile.Write(ref _ownerThread, null);
                        }
                    }
                    catch (Exception exception) // Just appeasing the tools telling me I must put an exception here
                    {
                        GC.KeepAlive(exception); // Just appeasing the tools telling me I am not using the exception, this is a noop.
                        // valueFactory did throw an exception
                        // We have the option to device a method to cache it and throw it in every calling thread
                        // However, I will be reverting to an uninitialized state
                        Volatile.Write(ref _status, 0);
                        // Reset event so that threads can wait for initialization
                        _event.Reset();
                        throw;
                        // Note: I know this is a weird configuration. Why is this catch not in the inner try?
                        // Because I want to make sure I set _status to 0 after the code from finally has run
                        // This way I am also sure that another thread cannot enter while finally is running, even if there was an exception
                        // In particular, I do not want Invalidate to call _event.Reset before we call _event.Set
                    }
                    return result;
                }
                // We didn't get the right to initialize
                // Another thread managed to enter first
            }
            // Another thread is initializing the value
            _event.Wait();
            // Perhaps there was an exception during initialization
            // We need to loop, if everything is ok, `ownerThread == null` and `Volatile.Read(ref _status) == 1`
            // Otherwise, we would want a chance to try to initialize
        }

        T Create()
        {
            // calling valueFactory, it could throw
            var created = valueFactory();
            Volatile.Write(ref _value, new StrongBox<T>(created));
            return created;
        }
    }

    public T Get()
    {
        var ownerThread = Volatile.Read(ref _ownerThread);
        if (ownerThread == Thread.CurrentThread)
        {
            // We have reentry, this is the same thread that is initializing on the value
            // We could:
            // 1. return default(T);
            // 2. throw new LockRecursionException()
            // We are doing the last one
            throw new LockRecursionException();
        }
        // Note: It is ok to loop here. Since Threads will only ever change _ownerThread to themselves...
        // there is no chance that _ownerThread suddently changes to the current thread unexpectedly
        // Why we loop? See at the bottom of the loop.
        while (true)
        {
            ownerThread = Volatile.Read(ref _ownerThread);
            if (ownerThread == null)
            {
                // There is no thread initializing currently
                // Either the value has already been initialized or not
                // Check status to tell
                if (Volatile.Read(ref _status) == 1)
                {
                    // Value has already been initialized
                    var box = Volatile.Read(ref _value);
                    if (box == null)
                    {
                        // Another thread invalidated after we did read _status but before we got Value
                        continue;
                    }
                    return box.Value;
                }
            }
            // Value is yet to be initialized, perhaps another thread is initializing the value
            _event.Wait();
            // Perhaps there was an exception during initialization
            // We need to loop, if everything is ok, `ownerThread == null` and `Volatile.Read(ref _status) == 1`
            // Otherwise, we keep waiting
        }
    }

    public void Invalidate()
    {
        var ownerThread = Volatile.Read(ref _ownerThread);
        if (ownerThread == null)
        {
            // There is no thread initializing currently
            // Either the value has already been initialized or not
            // Try to say it is not initialized
            var found = Interlocked.CompareExchange(ref _status, 0, 1);
            if (found == 1)
            {
                // We did set it to not intialized
                // Now we have the responsability to notify the value is not set
                _event.Reset();
                // And the privilege to destroy the value >:v (because we are using StrongBox<T> this is atomic)
                Volatile.Write(ref _value, null);
            }
        }
        // Either:
        // 1. Another thread is initializing the value. In this case we pretend we got here before that other thread did enter.
        // 2. The value is yet to be initialized. In this case we have nothing to do.
        // 3. Another thread managed to invalidate first. Let us call it a job done.
        // 4. This thread did invalidate. Good job.
        // 5. We have reentry
    }

    public bool TryGet(out T value)
    {
        var ownerThread = Volatile.Read(ref _ownerThread);
        if (ownerThread == null)
        {
            // There is no thread initializing currently
            // Either the value has already been initialized or not
            // Check status to tell
            if (Volatile.Read(ref _status) == 1)
            {
                // Value has already been initialized
                var box = Volatile.Read(ref _value);
                if (box != null)
                {
                    value = box.Value;
                    return true;
                }
            }
        }
        // Either:
        // 1. Another thread invalidated after we did read _status but before we got Value
        // 2. Value is yet to be initialized
        // 3. Another thread is initializing the value
        // 4. We have reentry
        value = default(T);
        return false;
    }
}

您可以看到,版本比较复杂。这将更加难以维护和调试。但是,它提供了更高级别的控制。例如,我设法为您提供了一个Get方法,该方法等待另一个线程初始化。我同意,这是一个不好的API……如果它是Task,那就更好了。
我确实尝试使用Task<T>来实现它,但是我做错了。我相信只有适当的异常缓存才有意义。
因此,让我们添加异常缓存并使用Task<T>
public class TaskLazy<T>
{
    private Thread _ownerThread;
    private TaskCompletionSource<T> _source;

    public TaskLazy()
    {
        _source = new TaskCompletionSource<T>();
    }

    public Task<T> Get(Func<T> valueFactory)
    {
        if (valueFactory == null)
        {
            throw new ArgumentNullException(nameof(valueFactory));
        }
        var source = Volatile.Read(ref _source);
        if (!source.Task.IsCompleted)
        {
            var ownerThread = Interlocked.CompareExchange(ref _ownerThread, Thread.CurrentThread, null);
            if (ownerThread == Thread.CurrentThread)
            {
                return Task.Run(valueFactory);
            }
            if (ownerThread == null)
            {
                try
                {
                    source.SetResult(valueFactory());
                }
                catch (Exception exception)
                {
                    source.SetException(exception);
                    throw; // <-- you probably want to throw here, right?
                }
                finally
                {
                    Volatile.Write(ref _ownerThread, null);
                }
            }
        }
        return source.Task;
    }

    public Task<T> Get()
    {
        var source = Volatile.Read(ref _source);
        if (!source.Task.IsCompleted)
        {
            var ownerThread = Volatile.Read(ref _ownerThread);
            if (ownerThread == Thread.CurrentThread)
            {
                throw new LockRecursionException();
            }
        }
        return source.Task;
    }

    public void Invalidate()
    {
        if (Volatile.Read(ref _source).Task.IsCompleted)
        {
            Volatile.Write(ref _source, new TaskCompletionSource<T>());
        }
    }
}

很好。通过消除以下想法:如果valueFactory发生故障,另一个机会运行,并用异常缓存替换它,我们可以消除循环,这使我们可以返回Task<T>,并且您可以免费获得WaitContinueWith之类的东西。 />
附录:
关于volatile,请阅读:

C#内存模型在理论和实践中,第2部分
C#中的线程,第4部分(高级线程)


评论


\ $ \ begingroup \ $
评论不用于扩展讨论;该对话已移至为另一个答案的相关讨论而创建的聊天室。
\ $ \ endgroup \ $
–Vogel612♦
18年11月18日在19:15



\ $ \ begingroup \ $
聊天中的名言:VisualMelon提供了有关volatile的出色链接,该链接描述了涉及双重volatile读取的一个缺陷,该缺陷在.NET 4.0之前已被打破(至少根据链接,我们没有对其进行测试)。 Theraot编辑了第四版下的示例,显示了使用volatile时必须非常小心,犯错的容易程度,一个很好的例子!最后,.NET Core使某些64bit / 128bit指令即使在32bit上也具有SSE2 / AVX指令(矢量化)是原子的,这使我们感到惊讶。
\ $ \ endgroup \ $
–user52292
18年11月20日在8:04

#2 楼


当前的实现是否“安全”?


绝对不是。您不得不问这个问题的事实表明您对线程建立这样的机制的了解不足。您需要对内存模型有深入而透彻的了解,才能构建这些机制。这就是为什么您应该始终依赖由专家编写的框架中提供的机制的原因。
为什么它不安全?请考虑以下情形。我们有两个线程A和B。_Valuenull_Loadedfalse


我们在线程A上。
_Value的内存位置已加载到处理器中线程A所属的CPU的高速缓存。是null
我们切换到线程B。
线程B将_Loaded读取为false,获取锁,再次检查_Loaded,调用create,分配_Value_Loaded并离开锁。
我们切换回线程A。

_Loaded现在为true,因此线程A从处理器缓存中返回_Value,该值为空。

不需要线程A来使缓存无效因为线程A永远不需要锁。!

现在,我在这里从处理器缓存中进行了论证。一般来说,这是错误的论点。而是,在尝试构建这样的新线程机制时,您必须做的是不推理任何特定的处理器体系结构,而是推理C#语言的抽象内存模型。 C#允许读写在多线程程序中随时间向前和向后移动。必须考虑C#规范未明确禁止的任何时间旅行。然后,您的任务是编写对读取和写入的时间移动的任何可能组合正确的代码,而不管它们是否确实在特定处理器上可行。

请注意,特别是C#规范并不要求所有线程都遵循一致的写入和读取重新排序集。完全合法,并且两个线程可能就如何相对于写入对读取进行重新排序存在分歧。时间听起来很难,那是因为。我没有胜任这项工作的能力,也没有尝试这样做。我将其留给专家。


与原始性能相比,还有什么重要的性能考虑事项吗?


只有您可以回答这个问题。通过收集现实世界的经验数据来回答性能问题。

但是,我通常可以对这个问题说几句话。

第一个是:双重检查锁定是目的是避免使用锁的费用。让我们检查该意图的基础假设。假定在无竞争的路径上进行锁定的成本太高。该假设是否必要?采取无竞争锁的费用是多少?你测量了吗?您是否将其与避免锁定检查的费用进行了比较? (由于避免锁的检查代码是错误的,因此对其性能进行测试实际上没有意义,因为如果我们不在乎正确性,我们总是可以编写更快的代码,但是仍然需要知道这种干预是否有所改善。)最重要的是,获取无竞争锁的成本是否与此代码的使用者有关?因为他们是利益相关者,他们的观点是相关的;他们对无竞争锁的价格怎么说?

假设无竞争锁的成本是相关的。那么,毫无疑问,争执锁的成本是非常重要的。您已经建立了一种可能竞争许多线程的机制!您在这里考虑了哪些替代方案?例如,通过确定可以在多个线程上调用create函数是可以的,您可以完全避免该锁-也许我们知道它便宜且幂等。然后,这些线程可以争用自己的内心内容来初始化该字段,并且我们可以使用互锁交换来确保我们获得一致的值。这样可以完全避免使用锁的成本,但是会产生另一种成本,并要求调用者传递幂等的创建者。

让我们考虑性能方面解决方案的其他方面。无论是否使用锁,都可以分配锁对象,并永久保留它。垃圾收集器有什么负担?对收集压力有什么影响?这些都与性能息息相关。再一次,请记住,这里的假设是我们非常担心进入和留下无竞争锁需要花费几纳秒的时间,因此我们愿意编写经过双重检查的锁。如果这些纳秒是相关的,那么肯定会花费额外的毫秒数是非常相关的!


还有其他我想念的地方吗?申请吗?


我不知道该怎么回答。

评论


\ $ \ begingroup \ $
mhmm ...这听起来很理智,但我仍然不确定如何将这些知识应用于现实生活中,或者只是用于此问题-也许您可以添加一些实用建议?目前,它更像是OP上的一个不知所措的不知道他们在做什么的人;-(我觉得这个答案不是很有帮助...对不起。它与您的其他帖子或博客的级别不同。
\ $ \ endgroup \ $
–t3chb0t
18-11-15在19:18



\ $ \ begingroup \ $
@ t3chb0t:关于实用建议,我给出了我所知的最佳实用建议。不要尝试使用自己的低级机制。使用专家建立的机制。
\ $ \ endgroup \ $
–埃里克·利珀特
18年15月15日在19:22

\ $ \ begingroup \ $
@EricLippert您可以认真对待代码质量,并且仍然以不会让Dirk感到“被烧毁”的方式来构成批评。我认为第一段特别屈从。
\ $ \ endgroup \ $
–CAT
18年11月16日在6:33

\ $ \ begingroup \ $
@CaTs:就是说,我注意到您是在回应音调,而不是对实际内容的回应。我和德克都缺乏正确完成这项工作的知识和能力,这是事实。最好的做法是正确使用有能力的人建立的工具。如果要强调这些事实会使人们感到不舒服,那么,这就是我愿意付出的代价,目的是清楚明确地表明,滚动自己的线程机制非常危险,而且很容易出错。
\ $ \ endgroup \ $
–埃里克·利珀特
18-11-16在6:51

\ $ \ begingroup \ $
StackOverflow的目的是创建可搜索的工件-一个问题和答案的库,可能对将来的程序员有用,并且也希望对原始提问者有用。如果CodeReview堆栈具有相同的目标,那么这是一个很好的答案。考虑到这是Eric Lippert(曾在C#设计团队中任职),添加代码段或其他建议将向错误的程序员发送错误消息:“如果我包含此代码段/遵循此模式,那么我的自制线程管理解决方案会很安全并且运作良好-埃里克·利珀特(Eric Lippert)这样说。”
\ $ \ endgroup \ $
–benj2240
18年11月16日在15:22

#3 楼


在高流量应用程序中我还有什么需要我担心的问题吗?


是的,您的Lazy<T>.Value不再是通用的,而是object,如果Func<T>返回一个值类型,然后将进行很多取消装箱操作。这可能会损害性能。

我认为LazyFactory.GetOrCreate<T>(...)会做得更好。

评论


\ $ \ begingroup \ $
@ t3chb0t,您好,谢谢。我可以通过使类本身实际(可选)通用来解决此问题。
\ $ \ endgroup \ $
–德克·波尔(Dirk Boer)
18-11-15在19:08



\ $ \ begingroup \ $
关于LazyFactory.GetOrCreate ,您是否还不需要将核心逻辑放入构造函数中?
\ $ \ endgroup \ $
–德克·波尔(Dirk Boer)
18年11月15日在19:08

\ $ \ begingroup \ $
@DirkBoer为什么在Get方法中使用通用参数而不是类型?它要求类的使用者表现良好,而不是允许编译器强制执行良好的行为。考虑:字符串s = _SubEvents.Get(()=>“ s”); int四十二个= _SubEvents.Get(()=> 42);
\ $ \ endgroup \ $
–phoog
18-11-15在20:15



\ $ \ begingroup \ $
@phoog它直接针对t3chb0t,但是对于减少冗长程度而言,只需将类声明更改为MyLazy 并更改私有对象_Value;私有T _Value;我认为这不会增加详细程度。
\ $ \ endgroup \ $
– jpmc26
18年15月15日在21:58

\ $ \ begingroup \ $
@phoog如果类名是样板的主要来源,那么OP的命名技术肯定存在严重问题。也许他们应该考虑使用名称空间或别名形式。 =)我认为问题在于传递初始化函数,出于合法原因,初始化函数可能会很长且很复杂。此类的示例用法也确实表明OP正在传递长lambda。
\ $ \ endgroup \ $
– jpmc26
18-11-15在22:14



#4 楼


意味着我必须将有关惰性属性的一半逻辑放入构造函数中,并添加更多样板代码。


这有点猜测,但我想你有XY问题。您正在尝试减少样板,但是可能有比您建议的方法更好的方法。

如果我理解正确,那么您的问题是您的类看起来像这样:

public class MyClass
{
    private Lazy<string> _MyStringValue;
    // ...

    public MyClass()
    {
        this._MyStringValue = new Lazy<string>(() => {
            var builder = new StringBuilder();
            builder.Append("a");
            // 50 more lines of expensive construction
            return builder.ToString();
        });

        // 100 more lines constructing OTHER lazy stuff
    }
}


关于建立价值细节的光泽;这只是一个例子。重要的一点是,您的构造函数中已包含了所有这些逻辑。

我认为您可以通过两种方法来缓解此问题:



为什么要进行参数化

为什么要将所有这些逻辑放入构造函数中?无论如何,您正在失去很多可重复使用性。因此,将这些事物参数设置为其他参数:

public class MyClass
{
    private Lazy<string> _MyStringValue;
    // ...

    public MyClass(Lazy<string> myStringValue)
    {
        this._MyStringValue = myStringValue;
    }
}



您可以将此构造逻辑嵌入方法中,然后将该方法传递给Lazy构造函数:

class MyStringValueMaker
{
    // Could be an instance method if that's more appropriate.
    // This is just for example
    public static string MakeValue()
    {
        var builder = new StringBuilder();
        builder.Append("a");
        // 50 more lines of expensive construction
        return builder.ToString();
    }
}


然后在其他地方:

var myClass = new MyClass(new Lazy<string>(MyStringValueMaker.MakeValue));



现在突然之间一切都变得井井有条,更可重用且更易于理解。

如果那不是您的班级原本的样子,那么,我认为您最好发布一个新问题,要求对原班级进行复习了解如何改进的想法。

#5 楼

我喜欢这个主意,但是您应该在评论中仔细解释它的工作原理。

请尝试以下操作:

  MyLazy myLazy = new MyLazy();

  int value1 = myLazy.Get(() => 42);
  Console.WriteLine(value1);

  int value2 = myLazy.Get(() => 65);
  Console.WriteLine(value2);


它可以正确打印出来:

42
42


但是即使我们知道所有答案都是42,也不是那么直观。问题显然是,您必须-或可以-每个对creator的调用都提供一个Get<T>(Func<T> creator)函数,并且该函数是任意的,但是只有第一个实际上有任何作用。

评论


\ $ \ begingroup \ $
关于此实现的怪异之处在于,以下代码可编译但由于运行时异常而失败:MyLazy myLazy = new MyLazy(); int value1 = myLazy.Get(()=> 42); Console.WriteLine(value1);字符串值2 = myLazy.Get(()=>“六十五”); Console.WriteLine(value2);。
\ $ \ endgroup \ $
–phoog
18年11月15日21:00

\ $ \ begingroup \ $
@phoog:这可能是因为Get(()=>“ sixty-five”)被解析为Get ,然后Get 然后尝试返回(string)(object)42。
\ $ \ endgroup \ $
–user52292
18-11-15在21:53



\ $ \ begingroup \ $
@firda是的,这就是原因。我在示例中的观点是要引起人们对设计方面的关注,该方面实质上是对泛型系统的滥用。值初始化后调用属性需要创建一个委托对象,该对象除了标识强制转换的目标类型外没有其他用途。泛型的要点是,这些检查可以在编译时完成(这应该在运行时节省几纳秒的时间),而这里并没有发生。
\ $ \ endgroup \ $
–phoog
18-11-15在22:01



#6 楼


是当前的实现“安全”吗?


不是吗,因为:


您没有实现Double-已正确检查锁定-您有两个字段(_Value_Loaded),而不是只有一个。
您添加了新功能-Invalidate-即使您修复了先前的问题(例如,通过装箱将值)。

学习的经验


总是优先选择众所周知的实现(例如System.Lazy<T>System.Threading.LazyInitializer),而不是自己的实现-线程/进程同步和加密是两个要掌握的最重主题,不要指望一天就能自己设计这些东西,要花好几年才能掌握!
优化之前的基准/配置文件-lock通常足够好,可以尝试例如System.Threading.SemaphoreSlim或ReaderWriterLockSlim可以加快速度,但是请注意,它可能会变得更糟-因此,再次:首先进行测试和测量,如果需要的话,请保持聪明,如果可以的话,请保持懒惰。
如果仍然希望编写自己的版本,那么至少要记住:


读写可以以任何方式重新排序(除非它们相互依赖,例如在赋值a = b + c中-取指令的顺序)不能保证bc,但是必须在计算后对a进行写操作。当同步涉及多个变量时,请格外小心!您可能会认为它起作用是因为您以某种顺序进行操作,但这是错误的!无法保证跨线程的顺序!

volatile仅保证写入顺序,而不保证其他线程会立即看到它们。编辑:如Voo所指出,那里的文档(来自Microsoft!)不完整。语言规范(10.5.3)指出,易失性读取具有获取语义,而易失性写入具有释放语义。个人说明:如果您甚至不信任文档,应该怎么做才对?但是我发现Volatile.Read可以提供非常有力的保证(在文档中):返回:读取的值。不管处理器数量或处理器缓存状态如何,此值都是计算机中任何处理器写入的最新值。 System.Threading.Interlocked也有一些好的方法。编辑:警告。.NET4.5可以重新排序易失性读,即使它不应该=>不信任volatile,除非您是专家(知道要做什么或只是在做实验,不要将volatile用于生产代码,您已被警告,请阅读此内容。)
我不是专家;)




编辑:看到您的第三次尝试,我将尝试给予您是我的版本,但我再说一遍:我不是专家;)

public class MyLazy<T>
{
    private class Box
    {
        public T Value { get; }
        public Box(T value) => Value = value;
    }

    private Box box;
    private Func<T> factory;
    private readonly object guard = new object();

    public MyLazy(Func<T> factory) => this.factory = factory;

    public T Value
    {
        get
        {
            var box = Volatile.Read(ref this.box);
            return box != null ? box.Value : GetOrCreate();
        }
    }

    private T GetOrCreate()
    {
        lock (guard)
        {
            var box = Volatile.Read(ref this.box);
            if (box != null) return box.Value;
            box = new Box(factory());
            Volatile.Write(ref this.box, box);
            return box.Value;
        }
    }

    public void Invalidate() => Volatile.Write(ref this.box, null);
    public void Invalidate(Func<T> factory) // bonus
    {
        lock (guard)
        {
            this.factory = factory;
            Volatile.Write(ref this.box, null);
        }
    }
}



在对Theraot的答案做出反应之前,重要的评论



我没有在lock中使用Invalidate(),因为尽管您可能在GetOrCreate在不同线程中调用其Volatile.Write之前就可以调用它,但是没有真正的冲突,它的行为方式是,Invalidate()是或在volatile读取之前或在volatile写入之后调用(行为就像在volatile读取之前一样,即使它们之间也是如此)。阅读有关获取/发布的更多信息,以了解我刚刚写的内容。
目前尚不清楚可以调用Invalidate多少次(并重新创建值)。我以这种方式编写它,您实际上可以多次构造该值,甚至可以更改函数(奖金Invalidate(Func...) ...在此编辑之前已经存在)。我以为这实际上可能是非常有用的功能-通过允许工厂创建不同的东西和/或将工厂更改为set来具有get和lazy Invalidate。也许不是OP所代表的,但是我确实在考虑如何实际使用它。
我认为phoog的回答是理所当然的-在每次访问时创建委托确实不是一个好主意。在Henrik的回答下,我们的注释(我和phoog的注释)中还有更多可能出现的问题(API将类型与调用绑定在一起,允许Get(() => 42)和更高版本的Get(() => "surprise")

对Theraot的回答的反应


OP的第二个和(编辑:第四个-是的,其中包含一个从ExecutionAndPublication不返回的缺陷)版本对我来说是正确的,因为获取/释放语义(感谢前面已经提到的Voo)。

对Theraot的两条重要评论:


编辑:该演示已更新,我原来的反应不再适用:该演示试图证明什么?您正在调用Init两次,而中间没有Invalidate,这意味着当_data已经是_initialized时,您正在写入true。我也很惊讶它不会在.NET Core中失败,因为失败不是因为volatile而是因为double-init(当_data表示有效时覆盖_initialized)。

至于volatile Framework vs.Core,我不确定在这里,但是我对此的感觉是volatile的原始描述是编译器(IL + JIT)不会重新排列读/写的顺序,因为它是为Windows和x86(强内存模型),但后来针对ARM(弱内存模型)进行了重新设计,将规范更改为获取/发布,这些规范最初是隐式的(x86的强内存模型)。最初的定义并不能保证有关CPU和缓存的任何内容,但是被假定为是后来规格得到了升级。我不是语言律师;)

最后的注释

我(大概)不会为单次无效单身人士scenairo更新我的代码,我很期待转发给某人解决有关volatile与.NET 1.1,.NET 2.0 + 、. NET Core或更改之处的混乱。我本人是在这里学习一些东西的(并在可能的地方提供帮助)。

编辑:我们同意,习得性语义/保留语义仍然有效,但仅从.NET 4.5开始,请阅读此内容。

评论


\ $ \ begingroup \ $
@DirkBoer:由于您可能会在此答案中找到原因(两个变量+ volatile + Invalidate),您的第二个版本也不安全。
\ $ \ endgroup \ $
–user52292
18年11月16日在17:32

\ $ \ begingroup \ $
我将“我不是专家”更改为“我们不是专家”。
\ $ \ endgroup \ $
– jpmc26
18年11月16日在18:31

\ $ \ begingroup \ $
@ jpmc26:“我们”范围太广,“您和我还不是专家,至少还不是专家”……这使我想起:“您越了解,您就知道您不懂得越多知道”。无论如何,我只想说:此列表绝不是完整的,仅阅读它就不会成为专家;)(而且我什至在某些方面可能是错误的)。其他一些链接:SO:易失性,互锁性与锁和System.Threading.Interlocked
\ $ \ endgroup \ $
–user52292
18年11月16日在18:51

\ $ \ begingroup \ $
正如我们在其他注释中已经讨论的那样,要点2极具误导性:volatile保证的不仅仅是写顺序。它提供可见性和订购保证。这也影响点1-挥发对于获得使多变量事物起作用的必要顺序保证是必不可少的。但是您可以使它起作用。
\ $ \ endgroup \ $
– Voo
18-11-17在9:06



\ $ \ begingroup \ $
关于为什么我没有在Invalidate()中使用锁的注意事项:您可能在GetOrCreate调用其Volatile.Write之前就调用了它,但是没有真正的冲突,但是它的行为方式使Invalidate ()在volatile读取之前或在volatile写入之后被调用(即使在volatile读取之间,其行为也一样)。但是请随时添加锁以提高安全性,请阅读有关获取/释放的更多信息以了解我刚刚写的内容。
\ $ \ endgroup \ $
–user52292
18年11月18日在8:55

#7 楼


在高流量应用程序中我还有什么需要我担心的问题?


通过在Get方法中传递委托,您正在实例化一个每次调用属性时都委托对象。 System.Lazy<T>仅创建一次委托实例。

#8 楼

为什么不包装Lazy<T>,然后将Lazy<T>延迟加载到Get中?充分利用已经尝试过并测试过的现有功能,而不是尝试自己动手使用。

评论


\ $ \ begingroup \ $
我听说你喜欢懒惰,所以我在你的懒惰中放了一些懒惰...;)
\ $ \ endgroup \ $
– Pieter Witvoet
18-11-15在13:02



\ $ \ begingroup \ $
如果您使用了ConcurrentDictionary,则不需要锁-它具有非常方便的GetOrAdd方法;-]
\ $ \ endgroup \ $
–t3chb0t
18-11-15在13:34



\ $ \ begingroup \ $
@ t3chb0t好点
\ $ \ endgroup \ $
– Nkosi
18年15月15日在13:35

\ $ \ begingroup \ $
没有驱逐策略的静态缓存称为内存泄漏。
\ $ \ endgroup \ $
– Johnbot
18年15月15日在13:42

\ $ \ begingroup \ $
@Johnbot是。我暂时将建议撤消。
\ $ \ endgroup \ $
– Nkosi
18年15月15日在13:43

#9 楼

有了(我的大脑)可以理解的反馈,我现在才来解决这个问题。


(我认为)我从字面上复制了惰性,线程安全的Singleton的锁定结构。 />
包括为_Loaded检查添加volatile关键字
将通用定义移动到类类型。利用更多类型安全性和禁止装箱的优势添加更多样板代码
添加警告以提醒自己可能存在问题

关于“将其交给更聪明的人”的建议”。那是我不能使用的东西。我喜欢学习,我喜欢其他人学习,并且我更喜欢这样一个社会:人们主动失败(反对计算成本)学习自己。

我感谢每个人对此都有不同的看法,没关系。

我仍然不能100%地确定这是否至少解决了第一个版本的线程安全性问题,因为对话变得有些离题了。如果有什么知识渊博的人可以对此发表评论,我将不胜感激。对于其余的;我将尝试使用此代码,并查看其在生产中的作用以及它是否对我的属性缓存造成(实际的)问题。

/// <summary>
/// Warning: might not be as performant (and safe?) as the Lazy<T>, see: 
/// https://codereview.stackexchange.com/questions/207708/own-implementation-of-lazyt-object
/// </summary>
public class MyLazy<T>
{
    private T               _Value;
    private volatile bool   _Loaded;
    private object          _Lock = new object();


    public T Get(Func<T> create)
    {
        if ( !_Loaded )
        {
            lock (_Lock)
            {
                if ( !_Loaded ) // double checked lock
                {
                    _Value   = create();
                    _Loaded = true;
                }
            }
        }

        return _Value;
    } 


    public void Invalidate()
    {
        lock ( _Lock )
            _Loaded = false;
    }
}


评论


\ $ \ begingroup \ $
至于建议“交给聪明的人”。那是我不能使用的东西。我完全支持这一点!如果以错误的方式做起来如此容易,那么也许应该有一些关于绝对不要的文档,如果使用错误的方式,该语言的哪些部分会很危险。没有这个问题和Eric的反馈,很多人可能永远不会听说C#规范未明确禁止的时间旅行,因此,绝对值得推广(并改进文档)。
\ $ \ endgroup \ $
–t3chb0t
18-11-16在11:16



\ $ \ begingroup \ $
我认为Eric的建议不是停止尝试试验和学习高级概念,而是尽可能从框架中依赖经过严格实践检验的正确实现。如果您想了解更多有关此方面的信息,我建议您这样做:albahari.com/threading/…
\ $ \ endgroup \ $
– RobH
18年11月16日在15:22

\ $ \ begingroup \ $
@ t3chb0t:对于密码学,最重要的建议是:不要自己动手。并不是因为有人认为您不应该学习,而是因为这是一个看似困难的主题,人们很容易以为自己做对了事情,但要真正做到正确则极其困难。我认为并发性也可以这么说(这是Eric所做的)。错误地记录错误并不容易,因为记录不充分,但这是一个非常困难的主题。
\ $ \ endgroup \ $
– Pieter Witvoet
18-11-16在15:31



\ $ \ begingroup \ $
@Adriano没有你的论点行不通。编译器在读取_Loaded之前可能无法访问_Value,因为_Loaded现在是volatile的,禁止重新排序(请注意Eric谈论的是不使用volatile的原始代码)。这里的问题是Invalidate,尽管它至少会导致竞争条件,这可能导致读取部分初始化的值。但是,如果您删除了Invalidate,就我所知,当前的实现应该可以。
\ $ \ endgroup \ $
– Voo
18-11-16在21:12



\ $ \ begingroup \ $
@Voo:易失性:添加volatile修饰符可确保所有线程将按照执行顺序对其他任何线程执行的易失性写入进行观察。它不保证有关任何其他变量/字段的任何信息(可以缓存,因为缓存行足够大,可以容纳_Value和恰好位于较低地址的某些对象的字段,您可以访问它们并使_Value加载到缓存)。
\ $ \ endgroup \ $
–user52292
18-11-16在22:06



#10 楼

我对Lazy的.NET Core版本进行了相同的分析,并删除了无论如何都不适合我这种情况的以下内容(new Lazy(Func<T>)构造函数)


异常缓存
然后是ExecutionAndPublication
其他所有模式
Contract.Assert的其他任何内容,例如Debugger可视化,ToString()等
内联了一些由于剥离而停止具有任何逻辑的功能其他事情
我已经颠倒了方法的顺序,使它更按时间顺序阅读

我所遇到的问题/说明:



如果在此剥离版本中将_state从对象更改为布尔值,会发生什么变化? (在原始版本中,它用于替代路径,例如不同的发布模式或缓存的异常),没关系-状态对象也被用作锁。我希望我们可以将其更改为对象吗?保存单独的类。
是否可以内联其他方法,例如ViaFactory?
让我感到有些奇怪的是,第一次以递归方式调用Value,但这可能与其他代码有关我剥离了-即异常缓存。

..

internal class LazyHelper
{
    internal LazyHelper()
    {
    }
}


public class Lazy<T>
{
    private volatile LazyHelper _state;
    private Func<T> _factory;
    private T _value;


    public Lazy(Func<T> valueFactory)
    {
        _factory = valueFactory;
        _state = new LazyHelper();
    }

    public T Value => _state == null ? _value : CreateValue();


    private T CreateValue()
    {
        var state = _state;
        if (state != null) 
        {
            ExecutionAndPublication(state);
        }
        return Value;
    }


    private void ExecutionAndPublication(LazyHelper executionAndPublication)
    {
        lock (executionAndPublication)
        {
            ViaFactory();
        }
    }


    private void ViaFactory()
    {
        _value = _factory();
        _state = null; // volatile write, must occur after setting _value
    }
}


不知道是否允许,但是如果我真的内联更多,获得带有挥发物的“正常”双重检查锁。主要区别在于,更衣室对象也被用作_loaded标志。

public class Lazy<T>
{
    private volatile object _state;
    private Func<T> _factory;
    private T _value;


    public Lazy(Func<T> valueFactory)
    {
        _factory = valueFactory;
        _state = new object();
    }

    public T Value => _state == null ? _value : CreateValue();


    private T CreateValue()
    {
        var state = _state;
        if (state != null) 
        {
            lock (state)
            {
                if (ReferenceEquals(_state, state)) // seems to act as the second check
                {
                    _value = _factory();
                    _state = null;
                }
            }
        }
        return Value;
    }
}


评论


\ $ \ begingroup \ $
1.记住这里没有无效,我给你的拥有它。 2. volatile关键字可能与Volatile.Read和Volatile.Write相同,但前者的文档更好,也可以转换为Visual Basic,因此,我宁愿使用方法代替关键字。 3. .NET Core似乎在初始化后释放了锁(再次:没有Invalidate),并使用了获取/释放语义,这是篱笆的(没有重新排序的意义,但是请您自己寻找,以获得完整的解释)。
\ $ \ endgroup \ $
–user52292
18年11月18日在8:29

\ $ \ begingroup \ $
T值=> _state ...具有获取语义(Volatile.Read),这意味着,直到在具有释放语义的CreateValue(Volatile.Write)中执行_state = null之前,它看不到null。在易失性读取之前重新排序(无法缓存值),并且在易失性写入之后(无法部分构造)无法对任何写进行重新排序。
\ $ \ endgroup \ $
–user52292
18年11月18日在8:40

#11 楼

我以为,当我问这个问题时,将与委托人的过关有关,但是似乎很难把握的是双重检查锁。

我没有停止好奇心,所以选择了。 NET的Lazy来源,并在这种情况下去除了不适合我的需求的以下内容:


异常缓存
然后是ExecutionAndPublication
所有其他模式
注释
Contract.Assert的其他任何内容,例如调试器可视化,ToString()等
内联了一些由于剥离其他内容而停止具有任何逻辑的功能

这是结果直到现在为止被剥离的懒惰。请注意,我尽力不改变任何可能改变行为的东西,希望不会。如果其他人有更新,那么欢迎您。

请注意,我这样做是为了学习和学习,而不是将其投入生产。

我仍然有一些问题必须进一步简化:在这种情况下,可以将try / finally和Monitor.Enter / Exit以及lockTaken简化为lock {}语句吗?如果我在这里遵循Eric的回答,那将是肯定的,但也许我忽略了一些事情:https://stackoverflow.com/a/2837224/647845可能不是,因为if语句。
可以替换前哨吗​​?
如果您认为逻辑良好,是否可以用内联new Boxed(factory())替换整个CreateValue()?

-
-

#if !FEATURE_CORECLR
[HostProtection(Synchronization = true, ExternalThreading = true)]
#endif
public class Lazy2<T>
{
    class Boxed
    {
        internal Boxed(T value)
        {
            m_value = value;
        }
        internal T m_value;
    }


    static readonly Func<T> ALREADY_INVOKED_SENTINEL = delegate 
    {
        return default(T);
    };

    private Boxed m_boxed;
    private Func<T> m_valueFactory;
    private object m_threadSafeObj;


    public Lazy2(Func<T> valueFactory)
    {
        m_threadSafeObj = new object();
        m_valueFactory = valueFactory;
    }


    public T Value
    {
        get
        {
            Boxed boxed = null;
            if (m_boxed != null )
            {
                boxed = m_boxed;
                if (boxed != null)
                {
                    return boxed.m_value;
                }
            }

            return LazyInitValue();
        }
    }

    private T LazyInitValue()
    {
        Boxed boxed = null;

        object threadSafeObj = Volatile.Read(ref m_threadSafeObj);
        bool lockTaken = false;

        try
        {
            if (threadSafeObj != (object)ALREADY_INVOKED_SENTINEL)
                Monitor.Enter(threadSafeObj, ref lockTaken);

            if (m_boxed == null)
            {
                boxed = CreateValue();
                m_boxed = boxed;
                Volatile.Write(ref m_threadSafeObj, ALREADY_INVOKED_SENTINEL);
            }
            boxed = m_boxed;
        }
        finally
        {
            if (lockTaken)
                Monitor.Exit(threadSafeObj);
        }

        return boxed.m_value;
    }

    private Boxed CreateValue()
    {
        Boxed boxed = null;

        Func<T> factory = m_valueFactory;
        m_valueFactory = ALREADY_INVOKED_SENTINEL;

        boxed = new Boxed(factory());

        return boxed;
    }
}


评论


\ $ \ begingroup \ $
您是否还打算添加Invalidate()=> Volatile.Write(ref m_boxed,null)?我宁愿使用Boxed boxed = Volatile.Read(ref m_boxed),将m_threadSafeObj设置为只读(使用锁并删除前哨)。但是请记住,我不是专家,只是感兴趣;)
\ $ \ endgroup \ $
–user52292
18-11-17在12:59



\ $ \ begingroup \ $
@firda,您好,尚未计划:)只是想将所有内容分解为核心。对于我自己的版本,如果我具有Invalidate()会很有用,但是我首先需要更好地了解所有活动部件。
\ $ \ endgroup \ $
–德克·波尔(Dirk Boer)
18年11月17日在13:07

\ $ \ begingroup \ $
就这样。一个volatile变量,而不是两个。
\ $ \ endgroup \ $
– Adriano Repetti
18-11-17在16:26



\ $ \ begingroup \ $
@Voo:在Reference Soruce中也没有volatile,并且此版本中也没有Invalidate,因此逻辑似乎是:1.在Boxed完全构建之前,不能分配m_boxed(它是一个类,而不是struct)和2.即使该效果并非对所有线程都立即可见,他们也只会采取锁定措施来发现它已经创建。
\ $ \ endgroup \ $
–user52292
18年11月18日在0:01

\ $ \ begingroup \ $
最好张贴一个后续问题,而不是多个版本作为其中的问题答案。
\ $ \ endgroup \ $
– Heslacher
18年11月19日在8:40