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

时间:2013-10-15 13:37:42

标签: c# multithreading performance dispatcher volatile

我已经使用了volatile,我不确定是否有必要。我很确定锁定在我的情况下会有点过分。阅读这个帖子(Eric Lippert评论)让我对使用volatile:When should the volatile keyword be used in c# ?

感到焦虑

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

我添加了“volatile”以确保不会发生未对齐:只读取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();
        }
    }

5 个答案:

答案 0 :(得分:62)

  

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

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

如果您不是100%清楚C#中的易失性含义,那么就不会使用这是一个非常好的工具,仅供专家使用。如果您无法描述当两个线程正在读取和写入两个不同的易失性字段时,弱内存模型体系结构允许所有可能的内存访问重新排序,那么您不知道足够安全地使用volatile而您将犯错误,因为您有在这里完成,写一个非常脆弱的程序。

  

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

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

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

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

只有在您通过删除争用无法解决的锁具有演示性能问题时,您才开始考虑使用低锁解决方案。

  

我添加了“volatile”以确保没有发生错位:只读取32位变量,另一个读取另外32位,这可以通过另一个线程中间的写入来分解。 / p>

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

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

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

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

第三,是的,挥发性访问确实是原子的,但这并不是因为C#将所有易失性访问都放入原子访问中!相反,C#将volatile置于字段上是非法的,除非该字段已经原子。

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

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

答案 1 :(得分:13)

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

此处强烈指出使用 lock ,不仅可以保护版本号,还可以防止对象在序列化时发生变化。你没有这样做会导致损坏的保存完全不常见,无法调试问题。

答案 2 :(得分:3)

根据Joe Albahari的优秀post on threads and locks来自他同样出色的书C#5.0 In A Nutshell,他说here即使使用volatile关键字,写入语句后跟读取声明可以重新排序。

更进一步说,他说关于这个主题的MSDN文档是不正确的,并且表明有一个强有力的案例要求完全避免使用volatile关键字。他指出,即使你碰巧理解所涉及的微妙之处,其他开发人员也能理解它吗?

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

答案 3 :(得分:1)

关于你的声明,当使用volatile时,变量是否可以在两个32位读取中拆分,如果你使用比Int32更大的东西,这可能是有可能的。

因此,只要您使用Int32,就不会出现问题。

但是,正如你在建议的链接中读到的那样,volatile只给你一些弱保证,我宁愿选择锁,以便安全,因为今天的机器有多个CPU,而且volatile不能保证另一个CPU赢了' t扫入并做一些意想不到的事情。

修改

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

答案 4 :(得分:0)

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