c# - 易失性关键字使用与锁定

Eri*_*let 29 c# performance multithreading volatile dispatcher

我已经使用了volatile,我不确定它是否有必要.我很确定锁定在我的情况下会有点过分.阅读这个帖子(Eric Lippert评论)让我对使用volatile感到焦虑:什么时候应该在c#中使用volatile关键字?

我使用volatile是因为我的变量在多线程上下文中使用,其中可以同时访问/修改此变量,但是我可以在没有任何伤害的情况下松散添加(参见代码).

我添加"挥发性",以确保有不发生未命中对准:在另一个取可以在2通过在从另一个线程中间的写入被打破仅读取32可变的比特和其他32位.

我先前的假设(先前的陈述)是否真的可以发生?如果没有,"挥发性"使用是否仍然是必要的(选项属性修改可能在任何线程中发生).

看了2个第一个答案.我想坚持代码编写方式的事实,如果由于并发性我们错过了一个增量(想要从2个线程递增但结果仅由于并发而增加1)并不重要变量'_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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 63

我已经使用了volatile,我不确定它是否有必要.

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

如果你不是100%清楚C#中的volatile意味着什么,那就不要使用它. 它是一种尖锐的工具,仅供专家使用.如果您无法描述当两个线程正在读取和写入两个不同的易失性字段时,弱内存模型体系结构允许所有可能的内存访问重新排序,那么您就不太了解安全使用volatile而您将犯错误,因为您有在这里完成,写一个非常脆弱的程序.

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

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

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

如果锁中的代码很昂贵,竞争锁的价格当然是任意高的.不要在锁内做昂贵的工作,因此争用的可能性很低.

只有当你有一个证明与性能问题不能被消除竞争来解决的锁,你应该甚至开始考虑低锁的解决方案.

我添加了"volatile"以确保没有发生错位:只读取32位变量和另一个32位,这可以通过另一个线程的中间写入来分解.

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

你的判决是荒谬的,因为:

首先,整数已经只有32位.

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

第三,是的,挥发性访问确实是原子的,但这不是因为C#使所有易失性访问成为原子访问!相反,C#将volatile放在字段上是非法的,除非该字段已经是原子的.

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

看看你的实际代码:正如汉斯指出的那样,volatile是完全不适合使代码正确的.最好的办法就是我之前说的:不要在主线程以外的任何线程上调用这些方法.反逻辑错误应该是你最不担心的.如果另一个线程上的代码在序列化时修改了对象的字段,那么是什么使序列化线程安全?这是你应该首先担心的问题.

  • @EricLippert很棒的帖子!虽然有点居高临下的语气([释义]我会在我的工作中使用你的代码作为编码时愚蠢的人做的一个例子).我们真的不需要那个. (6认同)
  • @EricLippert,虽然答案是元的,但我同意埃斯特布罗的观点,即它给人一种居高临下的感觉。我觉得如果你编辑一下你的答案,它可能会更简洁,也许信息更丰富。 (4认同)
  • @estebro:不打算屈尊俯就; 当聪明的人发布他们写的合理但破损的代码时,我真的很高兴.它帮助我和我的同事编写分析器来查找损坏的代码,从而使影响您的关键任务代码更可能是正确的. (3认同)
  • @EricOuellet:C#*语言*仅保证32位整数(或更小),bool,单精度浮点数,引用和指针的读写是原子的.(对齐时.)*运行时*可以保证64位操作系统上64位加倍和整数读写都是原子的,但这不是*语言*保证,那就是*运行时*. (2认同)
  • @estebro 据我所知,埃里克·利珀特的回答没有任何居高临下的意思。上面的代码非常糟糕,是教导如何不编写多线程代码的一个很好的例子。 (2认同)

Han*_*ant 13

易失性远远不足以使这段代码安全.你可以使用Interlocked.Increment()和Interlocked.CompareExchange()进行低级锁定,但是没有理由认为Save()是线程安全的.它确实看起来像是试图保存一个被工作线程修改的对象.

这里强烈指出使用,不仅是为了保护版本号,还为了防止对象在序列化时发生变化.你不会这样做的损坏的保存完全太罕见,以至于没有一个调试问题的镜头.