如果我有一个非常不可变的类型(所有成员都是只读的,如果它们是引用类型成员,那么它们也引用了非常不可变的对象).
我想在类型上实现一个惰性初始化属性,如下所示:
private ReadOnlyCollectionm_PropName = null; public ReadOnlyCollection PropName { get { if(null == m_PropName) { ReadOnlyCollection temp = /* do lazy init */; m_PropName = temp; } return m_PropName; } }
据我所知:
m_PropName = temp;
......是线程安全的.我并不担心两个线程同时竞争初始化,因为它很少见,从逻辑角度来看两个结果都是相同的,如果我没有,我宁愿不使用锁至.
这会有用吗?优缺点都有什么?
编辑: 谢谢你的回答.我可能会继续使用锁.但是,我很惊讶没有人提出编译器意识到临时变量是不必要的可能性,只是直接分配给m_PropName.如果是这种情况,则读取线程可能会读取尚未完成构造的对象.编译器是否会阻止这种情况?
(答案似乎表明运行时不会允许这种情况发生.)
编辑: 所以我决定使用由Joe Duffy撰写的这篇文章启发的Interlocked CompareExchange方法.
基本上:
private ReadOnlyCollectionm_PropName = null; public ReadOnlyCollection PropName { get { if(null == m_PropName) { ReadOnlyCollection temp = /* do lazy init */; System.Threading.Interlocked(ref m_PropName, temp, null); } return m_PropName; } }
这应该确保在此对象实例上调用此方法的所有线程都将获得对同一对象的引用,因此==运算符将起作用.有可能浪费工作,这很好 - 它只是使这成为一个乐观的算法.
如下面的一些评论中所述,这取决于.NET 2.0内存模型的工作原理.否则,m_PropName应声明为volatile.
我有兴趣听到其他答案,但我没有看到它的问题.副本将被放弃并获得GCed.
你需要创造这个领域volatile
.
关于这个:
但是,我很惊讶没有人提出编译器意识到临时变量是不必要的可能性,只是直接分配给m_PropName.如果是这种情况,则读取线程可能会读取尚未完成构造的对象.编译器是否会阻止这种情况?
我考虑过提到它,但没有区别.new运算符不返回引用(因此不会发生对字段的赋值),直到构造函数完成 - 这由运行时保证,而不是编译器.
但是,语言/运行时并不能确保其他线程无法看到部分构造的对象 - 它取决于构造函数的作用.
更新:
OP还想知道这个页面是否有一个有用的想法.他们的最终代码片段是Double checked locking的一个实例,这是一个想法的经典例子,成千上万的人推荐给对方而不知道如何正确地做到这一点.问题是SMP机器由几个带有自己的内存缓存的CPU组成.如果每次有内存更新时都必须同步它们的缓存,这将取消拥有多个CPU的好处.因此,它们仅在"内存屏障"处同步,这在锁定被取出或发生互锁操作或volatile
访问变量时发生.
通常的事件顺序是:
编码器发现双重检查锁定
编码器发现了内存障碍
在这两个事件之间,他们发布了许多破碎的软件.
此外,许多人认为(就像那个人那样)你可以通过使用互锁操作来"消除锁定".但是在运行时它们是一个内存屏障,因此它们会导致所有CPU停止并同步它们的缓存.它们优于锁定,因为它们不需要调用OS内核(它们只是"用户代码"),但它们可以像任何同步技术一样杀死性能.
总结:线程代码看起来比它更容易编写1000 x.
那可行.写入C#中的引用保证是原子的,如规范的第5.5节所述.这仍然可能不是一个好方法,因为您的代码将更加混乱调试和读取,以换取可能对性能的轻微影响.
Jon Skeet有一个关于在C#中实现singeltons 的很好的页面.
关于像这样的小优化的一般建议不是这样做,除非探查器告诉你这个代码是一个热点.此外,您应该警惕编写大多数程序员无法完全理解的代码而不检查规范.
编辑:正如评论中所指出的,即使你说你不介意你的对象的2个版本是否被创建,那么这种情况是非常直观的,以至于永远不应该使用这种方法.
你应该使用锁.否则,您将面临两个m_PropName
不同线程存在且正在使用的实例.在许多情况下这可能不是问题; 但是,如果你想能够使用==
而不是.equals()
那么这将是一个问题.罕见的竞争条件不是更好的错误.它们很难调试和重现.
在您的代码中,如果两个不同的线程同时获取您的属性PropName
(例如,在多核CPU上),那么它们可以接收包含相同数据但不是同一对象实例的属性的不同新实例.
不可变对象的一个主要好处==
是等同于.equals()
允许使用性能更高的对象==
进行比较.如果您在延迟初始化中未进行同步,则可能会失去此优势.
你也失去了不变性.您的对象将使用不同的对象(包含相同的值)初始化两次,因此已经获得属性值但又获得该属性的线程可能会第二次收到不同的对象.