35

我在不确定是否有必要的地方使用了 volatile。我很确定在我的情况下锁会是矫枉过正。阅读此线程(Eric Lippert 评论)让我对 volatile 的使用感到焦虑:何时应在 c# 中使用 volatile 关键字?

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

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

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

在阅读了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();
        }
    }
4

5 回答 5

65

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

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

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

我很确定在我的情况下锁会过大

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

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

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

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

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

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

你的句子很荒谬,因为:

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

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

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

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

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

于 2013-10-15T15:09:20.193 回答
15

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

这里非常强烈地指出使用,不仅是为了保护版本号,也是为了防止对象在序列化时发生变化。不这样做会导致损坏的保存非常罕见,以至于无法调试问题。

于 2013-10-15T14:14:08.353 回答
3

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

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

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

于 2015-02-15T05:10:14.893 回答
1

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

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

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

编辑

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

于 2013-10-15T13:51:27.217 回答
0

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

于 2013-10-15T13:51:01.827 回答