繁体   English   中英

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

[英]c# - Volatile keyword usage vs lock

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

我使用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();
        }
    }

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

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

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

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

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

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

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

只有当您通过删除争用无法解决锁定的已证明性能问题时,您才开始考虑使用低锁解决方案。

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

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

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

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

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

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

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

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

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

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

根据Joe Albahari 关于线程和锁的优秀帖子来自他同样出色的书C#5.0 In A Nutshell,他在这里说,即使使用volatile关键字,也可以重新排序后跟read语句的write语句。

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

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

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

因此,只要您使用Int32,就不会有任何问题。

但是,正如你在建议的链接中读到的那样,volatile只给你一些弱保证,我更喜欢锁,以便安全,因为今天的机器有多个CPU,而volatile不能保证另一个CPU不会扫描并做一些意外的事情。

编辑

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM