深度不可变类型的惰性初始化是否需要锁定?如果我有一个非常不可变的类型(所有成员都是只读的,如果是引用类型成员,它们也引用非常不可变的对象)。我想在这样的类型上实现一个惰性初始化属性:privateReadOnlyCollectionm_PropName=null;publicReadOnlyCollectionPropName{get{if(null==m_PropName){ReadOnlyCollectiontemp=/*dolazyinit*/;m_PropName=temp;}返回m_PropName;据我所知:m_PropName=temp;...是线程安全的。我不担心两个线程同时竞争初始化,因为这种情况很少见,从逻辑的角度来看,两个结果是相同的,如果不这样做,我宁愿不使用lock-to。这行得通吗?优缺点都有什么?编辑:感谢您的回答。我可能会继续使用锁。然而,我很惊讶没有人提出编译器意识到临时变量是不必要的并且直接分配给m_PropName的可能性。如果是这种情况,读取线程可能正在读取一个尚未完成构建的对象。编译器会阻止这种情况吗?(答案似乎表明运行时不允许这种情况发生。)编辑:所以我决定使用受JoeDuffy的这篇文章启发的InterlockedCompareExchange方法。基本上:privateReadOnlyCollectionm_PropName=null;publicReadOnlyCollectionPropName{get{if(null==m_PropName){ReadOnlyCollectiontemp=/*dolazyinit*/;System.Threading.Interlocked(refm_PropName,temp,null);}返回m_PropName;这应该确保在这个对象实例上调用这个方法的所有线程都将获得对同一个对象的引用,所以==运算符将起作用。可能会浪费工作,这很好-它只是使它成为一个乐观的算法。正如下面一些评论中提到的,这取决于.NET2.0内存模型的工作方式。否则,m_PropName应声明为可变的。这样可行。如规范第5.5节所述,用C#编写的引用保证是原始的。这仍然可能不是一个很好的方法,因为您的代码调试和阅读起来会更加混乱,以换取对性能的轻微影响。JonSkeet有一个关于在C#中实现singeltons的很棒的页面。关于像这样的小优化的一般建议是不要这样做,除非探查器告诉您代码是热点。此外,您应该警惕编写大多数程序员在不检查规范的情况下无法完全理解的代码。编辑:正如评论中所指出的,即使您说您不介意创建该对象的两个版本,这种情况也是违反直觉的,因此永远不应该使用这种方法。你应该使用锁。否则,您可能会有两个m_PropName实例被不同线程使用的风险。在许多情况下这可能不是问题;但是,如果您希望能够使用==而不是.equals()那么这将是一个问题。罕见的竞争条件并不是更好的错误。它们很难调试和重现。在您的代码中,如果两个不同的线程同时获取您的属性PropName(例如,在多核CPU上),那么它们可以接收包含相同数据但不是相同数据的属性的不同新实例目的。不可变对象的一个??主要好处是==等同于.equals(),允许使用==进行更高效的比较。如果您不在延迟初始化中同步,您可能会失去这个优势。你也失去了不变性。您的对象将使用不同的对象(包含相同的值)进行两次初始化,因此已经获得属性值但随后获得该属性的线程可能第二次接收到不同的对象。我很想听听其他答案,但我看不到它的问题。副本将被丢弃并进行GC。您需要使该字段易变。关于这个:然而,我很惊讶没有人提出编译器意识到临时变量是不必要的并且直接分配给m_PropName的可能性。如果是这种情况,读取线程可能正在读取一个尚未完成构造的对象。编译器会阻止这种情况吗?我考虑过提及它,但没有任何区别。在构造函数完成之前,new运算符不会返回引用(因此不会发生对该字段的赋值)——这是由运行时而非编译器保证的。但是,语言/运行时并不能确保其他线程看不到部分构造的对象——这取决于构造函数的作用。更新:OP还想知道此页面是否有有用的想法。他们最终的代码片段是双重检查锁定的一个实例,这是一个经典的例子,成千上万的人互相建议他们不知道如何正确地做。问题是SMP机器由几个CPU组成,它们有自己的内存缓存。如果每次内存更新时都必须同步它们的缓存,这将抵消拥有多个CPU的好处。所以它们只在“内存屏障”处同步,这发生在锁被取出或发生互锁操作或访问易失变量时。通常的事件顺序是:在这两个事件之间,他们发布了很多损坏的软件。此外,许多人认为(就像那个人那样)您可以通过使用互锁操作来“消除锁定”。但在运行时它们是一个内存屏障,因此它们会导致所有CPU停止并同步它们的缓存。它们比锁更好,因为它们不需要调用操作系统内核(它们只是“用户代码”),但它们会像任何同步技术一样降低性能。总之:线程代码看起来比实际更容易编写1000倍。当数据可能无法始终访问并且可能需要大量资源来获取或存储数据时,我完全赞成延迟初始化。我认为这里忘记了一个关键概念:根据C#设计概念,默认情况下不应使实例成员线程安全。默认情况下,只有静态成员应该是线程安全的。除非您正在访问某些静态/全局数据,否则不应向您的代码添加额外的锁。从您的代码显示的内容来看,lazyinit都在实例属性中,因此我不会为其添加锁。如果按照设计它意味着同时被多个线程访问,那么继续并添加锁。顺便说一句,它可能不会减少代码,但我是空合并运算符的粉丝。getter的主体可能变成这样:m_PropName=m_PropName??新的...();返回m_PropName;它去掉了额外的“if(m_PropName==null)...”,在我看来,它更简洁易读。我不是C#专家,但据我所知,如果您只需要创建一个ReadOnlyCollection实例,这只会导致问题。您说创建的对象将始终相同,两个(或更多)线程是否实际创建一个新实例并不重要,所以我说可以在没有锁的情况下进行。可能成为奇怪错误的一件事是,如果比较实例是否相等,有时是不相同的。但是,如果您牢记(或根本不牢记)这一点,我看不出有任何其他问题。不幸的是,你需要一把锁。当你没有正确锁定时,会有很多相当微妙的错误。有关令人生畏的示例,请参阅此答案。如果该字段只是空白或已经包含要写入的值,或者在某些情况下是等效字段,则使用延迟初始化而不锁定是安全的。请注意,没有两个可变对象是等价的;持有可变对象引用的字段只能写入对同一对象的引用(意味着写入无效)。根据情况,惰性初始化有三种通用模式:如果计算要写入的值的开销很大,并且您希望避免不必要地花费这种精力,则使用锁定。双重检查锁定模式在内存模型支持的系统上可用。如果存储不可变值,则在必要时计算它,然后存储它。其他看不到存储的线程可能会执行冗余计算,但它们只会尝试用已经存在的值写入字段。如果存储对廉价生成的可变类对象的引用,则在需要时创建一个新对象,然后在该字段仍为空时使用Interlocked.CompareExchange来存储它。请注意,如果您可以避免锁定线程中第一个访问以外的任何访问,则使惰性读取器线程安全不会产生任何显着的性能成本。虽然可变类通常不是线程安全的,但所有声称不可变的类对于读取器操作的任何组合都应该是100%线程安全的。任何不满足此线程安全要求的类都不应声称是不可变的。这绝对是个问题。考虑这种情况:线程“A”访问属性,并初始化集合。线程“B”访问该属性,直到将本地实例分配给字段“m_PropName”,除非它完成。线程“B”现在有对该实例的引用,该实例当前存储在“m_PropName”中...直到线程“A”继续,此时“m_PropName”被该线程中的本地实例覆盖。现在有几个问题。首先,线程“B”不再具有正确的实例,因为拥有对象认为“m_PropName”是唯一的实例,但当线程“B”在线程“A”之前完成时,它会泄漏已初始化的实例。另一个是如果集合在线程“A”和线程“B”获得它们的实例之间发生变化。然后你有不正确的数据。如果您在内部观察或修改一个只读集合(当然,您不能使用ReadOnlyCollection,但如果您将其替换为其他一些您可以通过事件观察或在内部而不是外部修改的实现,情况可能会更糟)。以上是C#学习教程:Doeslazyinitializationondeepimmutabletypesrequirelocking?如果所有分享的内容对你有用,需要进一步了解C#学习教程,希望大家多多关注。本文收集自网络,不代表立场。如涉及侵权,请点击右侧联系管理员删除。如需转载请注明出处:
