c# - 可变关键字使用 vs 锁定

Posted

技术标签:

【中文标题】c# - 可变关键字使用 vs 锁定【英文标题】:c# - Volatile keyword usage vs lock 【发布时间】:2013-10-23 08:16:43 【问题描述】:

我在不确定是否有必要的地方使用了 volatile。我很确定在我的情况下锁会是矫枉过正。阅读这个帖子(Eric Lippert 评论)让我对 volatile 的使用感到焦虑:When should the volatile keyword be used in c# ?

我使用 volatile 是因为我的变量在多线程上下文中使用,在该上下文中可以同时访问/修改该变量,但在其中我可以松散添加而不会造成任何伤害(参见代码)。

我添加了“易失性”以确保不会发生未对齐:仅读取变量的 32 位和另一个 fetch 上的其他 32 位,这可以通过在中间从另一个线程写入来打破。

我之前的假设(之前的陈述)真的会发生吗?如果不是,是否仍然需要使用“易失性”(选项属性修改可能发生在任何线程中)。

阅读前 2 个答案后。我想坚持这样一个事实,即代码的编写方式,如果由于并发性我们错过了一个增量(想从 2 个线程增加,但结果由于并发性只增加一个),这并不重要,如果至少变量“_actualVersion”递增。

作为参考,这是我使用它的代码部分。它仅在应用程序空闲时报告保存操作(写入磁盘)。

public abstract class OptionsBase : NotifyPropertyChangedBase

    private string _path;

    volatile private int _savedVersion = 0;
    volatile private int _actualVersion = 0;

    // ******************************************************************
    void OptionsBase_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    
        _actualVersion++;
        Application.Current.Dispatcher.BeginInvoke(new Action(InternalSave), DispatcherPriority.ApplicationIdle);
    

    // ******************************************************************
    private void InternalSave()
    
        if (_actualVersion != _savedVersion)
        
            _savedVersion = _actualVersion;
            Save();
        
    

    // ******************************************************************
    /// <summary>
    /// Save Options
    /// </summary>
    private void Save()
    
        using (XmlTextWriter writer = new XmlTextWriter(_path, null))
        
            writer.Formatting = Formatting.Indented;
            XmlSerializer x = new XmlSerializer(this.GetType());

            x.Serialize(writer, this);
            writer.Close();
        
    

【问题讨论】:

不是您所要求的(因此添加为评论),但我可能会将 Save() 方法调用移至“_savedVersion = _actualVersion”行上方。这样,如果 Save() 抛出异常,_savedVersion 变量就不会被错误地更新。 为什么要从Dispatcher线程事件中保存?! @Baldrick,由于多线程的含义,我无法按照您所说的(移动)进行操作(如果这样做,我可能会错过版本更改)。但是你部分正确,我应该防止我的代码出现异常。谢谢! @Ahmed,好问题。在大多数情况下,我的 OptionBase 类将从 UI 线程进行修改,并且将同时修改许多属性。这样,我可以保证每次属性修改只保存一次。我想从界面中提取负载并保存(将其隐藏给用户)。它应该更易于使用。选项也可以在代码中的任何地方一一修改,通常在 UI 线程中。我更愿意确保不要一直管理保存。 @EricOuellet - 我发现这个答案最能帮助您了解它们如何协同工作 - ***.com/questions/154551/… 【参考方案1】:

我在不确定是否有必要的地方使用了 volatile。

让我在这一点上非常清楚:

如果您不是 100% 清楚 volatile 在 C# 中的含义,那么不要使用它这是一个仅供专家使用的锋利工具.如果您无法描述当两个线程正在读取和写入两个不同的 volatile 字段时,弱内存模型架构允许的所有可能的内存访问重新排序,那么您对安全使用 volatile 的了解不足,并且您会犯错误,因为您有到这里就搞定了,写一个非常脆弱的程序。

我很确定在我的情况下锁会有点过分

首先,最好的解决方案是干脆不去那里。如果您不编写尝试共享内存的多线程代码,那么您就不必担心锁定,这很难得到正确。

如果您必须编写共享内存的多线程代码,那么最佳做法是始终使用锁。锁几乎从不矫枉过正。非竞争锁的价格大约为 10 纳秒。你真的是在告诉我额外的十纳秒会对你的用户产生影响吗?如果是这样,那么您有一个非常非常快的程序和一个具有异常高标准的用户。

如果锁内的代码很昂贵,那么竞争锁的价格当然是任意高的。 不要在锁内做昂贵的工作,这样争用的概率就会很低。

只有当您证明性能问题无法通过消除争用来解决时,您才应该开始考虑低锁解决方案。

我添加了“易失性”以确保不会发生错位:仅读取变量的 32 位和另一个 fetch 上的其他 32 位,可以通过在中间从另一个线程的写入将其分成两部分。

这句话告诉我,你现在需要停止编写多线程代码。多线程代码,尤其是低锁代码,仅供专家使用。在再次开始编写多线程代码之前,您必须了解系统的实际工作原理。找一本关于该主题的好书并努力学习。

你的句子是无意义的,因为:

首先,整数已经只有 32 位了。

其次,规范保证 int 访问是原子的!如果你想要原子性,你已经得到了。

第三,是的,易失性访问总是原子的,但这并不是因为 C# 将所有易失性访问都变成了原子访问!相反,C# 将 volatile 放在字段上是非法的,除非该字段已经是原子的。

第四,volatile 的目的是防止 C# 编译器、抖动和 CPU 进行某些优化,这些优化会改变你的程序在弱内存模型中的含义。特别是易失性不会使 ++ 原子化。 (我在一家制造静态分析器的公司工作;我将使用您的代码作为我们的“易失性字段上不正确的非原子操作”检查器的测试用例。获得充满真实世界的代码对我很有帮助现实的错误;我们想确保我们确实找到了人们编写的错误,所以感谢发布这个。)

查看您的实际代码:正如 Hans 所指出的,volatile 完全不足以使您的代码正确。最好的办法就是我之前说过的:不要让这些方法在主线程以外的任何线程上调用。计数器逻辑错误应该是您最不担心的问题。 如果另一个线程上的代码在对象被序列化时修改了对象的字段,那么是什么让序列化线程安全?这是您首先应该担心的问题。

【讨论】:

@EricOuellet:C# 语言 只保证 32 位整数(或更小)、布尔值、单精度浮点数、引用和指针的读写是原子的。 (对齐时。)runtime 可以保证 64 位双精度和整数读写在 64 位操作系统上是原子的,但这不是 语言 使保证,这就是 runtime @EricLippert 好帖子!虽然有时有点居高临下([释义]我将在我的工作中使用你的代码作为愚蠢的人在编码时所做的一个例子)。我们真的不需要那个。 @estebro:没有居高临下的意思;当聪明人发布他们编写的看似合理但被破坏的代码时,我真的很高兴。它可以帮助我和我的同事编写分析器来查找损坏的代码,从而使影响您的关键任务代码更有可能是正确的。 @estebro 在我看来,Eric Lippert 的回答没有任何居高临下的地方。上面的代码很破,是一个很好的教导如何不要写多线程代码的例子。 @EricLippert,虽然答案是元数据,但我同意 estebro 的观点,认为它居高临下。我觉得如果你编辑你的答案,它可能会更简洁,也许更多信息。【参考方案2】:

Volatile 严重不足以保证这段代码的安全。您可以通过 Interlocked.Increment() 和 Interlocked.CompareExchange() 使用低级锁定,但几乎没有理由假设 Save() 是线程安全的。它看起来确实像是试图保存一个正在被工作线程修改的对象。

这里非常强烈地指出使用lock,不仅是为了保护版本号,也是为了防止对象在序列化时发生变化。不这样做会导致损坏的保存完全没有机会调试问题。

【讨论】:

我部分同意。同意按照你说的修改我的代码就够了。但是我本来可以在不连贯的保存中幸存下来,但让我真正害怕的是在我的保存过程中出现异常……我也可以编写代码来防止这种情况发生,但是锁定在这里成为一个明显的候选者。谢谢!【参考方案3】:

根据 Joe Albahari 的出色 post on threads and locks(来自他同样出色的书 C# 5.0 In A Nutshell),他说 here 即使使用 volatile 关键字,也可以重新排序后跟 write 语句的 write 语句.

进一步说,他说关于这个主题的 MSDN 文档是不正确的,并建议有充分的理由完全避免使用 volatile 关键字。他指出,即使你碰巧了解其中的微妙之处,其他开发人员也会明白吗?

因此,使用锁不仅更“正确”,而且更易于理解,可以轻松添加新功能,以原子方式将多个更新语句添加到代码中 - 这既不是 volatile 也不是栅栏类MemoryBarrier 可以做到,速度非常快,并且更容易维护,因为经验不足的开发人员引入细微错误的可能性要小得多。

【讨论】:

【参考方案4】:

关于您的声明是否可以在使用 volatile 时将变量拆分为两个 32 位提取,如果您使用大于 Int32 的东西,这可能是一种可能性。

因此,只要您使用 Int32,您所陈述的内容就没有问题。

但是,正如您在建议的链接中所读到的那样,volatile 只能为您提供微弱的保证,我更喜欢锁以便安全起见,因为今天的机器有多个 CPU,而 volatile 不能保证另一个 CPU 会赢。不要一头扎进去,做一些意想不到的事情。

编辑

您是否考虑过使用 Interlocked.Increment?

【讨论】:

谢谢白痴,我真的很喜欢你的回答。我更喜欢更深一点的东西。我会等几天,如果没有更深层次的事情发生,我会让你的接受。【参考方案5】:

无论有没有 volatile 关键字,InternalSave() 方法中的比较和赋值都不是线程安全的。如果您想避免使用锁,可以在框架的 Interlocked 类的代码中使用 CompareExchange() 和 Increment() 方法。

【讨论】:

Interlocked.Increment 可以很好地处理增量,但CompareExchange 不会在InternalSave 方法中处理竞争条件。在我看来他需要一把锁。 这里的竞争条件不是问题,因为代码的编写方式。但就像 Hans 所说,如果我在从其他线程修改属性时不使用 lock 来防止保存发生,我可能会保存不连贯的数据。 @Jim,保存本身只能发生在 UI 线程(只有一个线程)上,默认情况下是安全的。如果您在谈论其他代码和保存之间的关系,那么是的,您是对的,我需要一个锁。谢谢,埃里克。

以上是关于c# - 可变关键字使用 vs 锁定的主要内容,如果未能解决你的问题,请参考以下文章

用C#的params关键字实现方法形参个数可变示例

C#中lock死锁

多线程(基础篇3)

[c#]params可变参数

关键部分 - 只有单线程在锁定时“休眠”

C# params传递多个参数