易变变量有用吗?如果有的话,什么时候有用?

13 浏览
0 Comments

易变变量有用吗?如果有的话,什么时候有用?

回答这个问题让我思考了一些仍然不清楚的问题。首先假设我们已经阅读了来自这个帖子和这个帖子的所有内容。

[开始编辑]也许这并不明显(意大利幽默?!),但标题只是很具挑衅性:当然,如果把volatile包含在C#中,肯定有原因,我只是无法理解具体是什么原因。[结束编辑]

简而言之,我们知道我们有三种方法来在线程之间共享变量:

  • lock,因为这会防止指令重排序。
  • volatile,因为它会强制CPU始终从内存中读取值(然后不同的CPU/核心不会缓存它,它们不会看到旧的值)。
  • Interlocked操作(Increment/DecrementCompareExchange),因为它们将在单个原子(比例如volatile + lock更快)操作中执行更改+赋值。
  • 我不明白的是(如果有C#规范的参考将不胜感激):

    • 锁如何防止缓存问题?在临界区中是否隐含了内存屏障?
    • volatile变量不能是局部的(我读到了Eric Lippert的一些关于这个的内容,但现在找不到那篇帖子了,而且我也不太记得他的评论 - 老实说 - 我也没有完全理解)。这让我想到它们没有使用Interlocked.CompareExchange()等方法实现,它们有什么区别?

    例如,volatile修饰符在这段代码中会做什么?

    volatile int _volatileField = 0;
    int _normalField = 0;
    void test()
    {
        Interlocked.Increment(ref _normalField);
        ++_volatileField;
    }
    

    [开始编辑]之前的示例涉及原子读取+写入,让我们将其更改为_volatileField = 1;,在这里我不谈论原子操作。[结束编辑]

    此外,编译器(除了警告)在这里会做什么:

    Interlocked.Increment(ref _volatileField);
    

    它们似乎是非常不同的东西(正如我所想象的那样),但根据我的理解,Interlocked.Increment()的操作数应该隐含为volatile(然后它只会进行原子递增)。那么对于非volatile字段来说,这是如何可能的?它们也意味着屏障吗?这不会对性能造成很大的损害吗(与volatile相比)?

    如果volatile不意味着屏障,而其他操作则是,那么为什么我们不能像对局部变量一样使用它们?特别是在并行循环中使用时,这将严重影响性能(我在考虑在大量数据上工作的少量代码的小函数)数据缓存可能被很好地使用)。

    [开始编辑]我发现之前的句子真的很不清楚(对不起我的英语)。我的意思是:如果性能(与适用比较的volatileCompareExchange相比)更好(是的,我们可以测量,在某些情况下差异是可测量和可见的),那么为什么我们不能将它们用于局部变量?我在考虑处理大量数据的并行循环(其中开销和屏障都可能严重影响性能)。[结束编辑]

    0
    0 Comments

    volatile变量在多线程编程中非常有用。如果一个变量被声明为volatile,那么它的读取和写入操作就会具有内存屏障的语义。内存屏障描述了操作结果变得可用的顺序,acquire和release语义分别表示操作结果在代码中出现在之后或之前的操作结果之前或之后变得可用,而fence语义则是acquire和release语义的组合,表示操作结果在代码中出现在之后的操作结果之前,同时也在代码中出现在之前的操作结果之后。

    与volatile不同的是,lock关键字可以防止缓存问题。在临界区内,lock关键字同时具有acquire和release语义,即它既可以保证在临界区内操作的结果在之后的操作结果之前变得可用,也可以保证在临界区内操作的结果在之前的操作结果之后变得可用。

    尽管volatile变量的读取和写入操作具有原子性,但它们并不能防止其他线程对同一内存的竞争条件。即使volatile变量的读取/修改/写入操作的每个步骤都是原子的,但整个操作并不是原子的。在这种情况下,volatile关键字只能确保在读取/修改/写入操作开始时,读取操作确实从内存中获取数据,而不是从处理器缓存中获取数据。但是,如果存在其他并发写入该变量的线程,volatile关键字无法解决竞争条件问题。

    关于volatile变量不能在局部变量中使用的问题,这是因为局部变量的生命周期很短,而volatile关键字的作用是保证变量在线程之间的可见性。因此,编译器团队认为在局部变量中使用volatile关键字并不值得去做。

    至于为什么Interlocked操作不需要volatile输入,这是因为Interlocked操作本身已经同时具有内存屏障和原子性。编译器给出的警告只是一个泛型警告,因为它不知道Interlocked操作已经明确地处理了一切。关于这个问题的更多讨论可以参考链接:http://stackoverflow.com/questions/425132。

    volatile变量在多线程编程中是有用的,通过保证变量的可见性和防止缓存问题,可以避免竞争条件和数据不一致的问题。然而,需要注意的是,volatile关键字并不能解决所有的多线程问题,有些情况下可能需要使用更高级的同步机制来保证线程安全性。

    0
    0 Comments

    volatile变量在以下代码中理论上是有用的:

    while (myVolatileFlag)
        ...
    

    如果将myVolatileFlag声明为volatile bool,它将防止编译器缓存其值并假设在循环期间它不会改变。(然而,实际上很难编写一些能够展示应用volatile的差异的代码。)

    来自http://msdn.microsoft.com/en-us/LIBRARY/x13ttww7%28v=vs.80%29.aspx

    volatile关键字表示一个字段可能被多个并发执行的线程修改。声明为volatile的字段不受编译器优化的影响,这些优化假设只有一个线程访问。这确保了该字段始终包含最新的值。

    以下是一个演示该问题的示例程序:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    namespace Demo
    {
        internal class Program
        {
            private void run()
            {
                Task.Factory.StartNew(resetFlagAfter1s);
                int x = 0;
                while (flag)
                    ++x;
                Console.WriteLine("Done");
            }
            private void resetFlagAfter1s()
            {
                Thread.Sleep(1000);
                flag = false;
            }
            private volatile bool flag = true;
            private static void Main()
            {
                new Program().run();
            }
        }
    }
    

    运行上述程序的“Release”版本,它将在一秒钟后终止。将volatile修饰符从volatile bool flag移除,它将永远不会终止。

    volatile局部变量

    一般来说,对于局部变量来说,不需要使用volatile,因为编译器可以看到你是否修改了一个局部变量,或者是否将局部变量的引用传递给另一个方法。在这两种情况下,编译器将假设该值正在改变,并禁用依赖于值不变的优化。

    然而,在C#的较新版本中,例如使用Lambda表达式等情况下,情况就不那么明确了。请参考此线程中Eric Lippert的回答。

    这是volatile修饰符的唯一用途吗(类似于C寄存器修饰符的反义)?在这种情况下,为什么它不能用于局部变量?它与CompareExchange()有什么不同?原子操作是否隐含了屏障?如果是的话,volatile的性能更好,为什么不能用于局部变量?

    因为编译器可以看到你是否修改了一个局部变量,或者是否将局部变量的引用传递给另一个方法,所以不需要对局部变量使用volatile。在这两种情况下,编译器将假设该值正在改变,并禁用依赖于值不变的优化。

    <继续>然而,在C#的较新版本中,例如使用Lambda表达式等情况下,情况就不那么明确了。请参见此线程中Eric Lippert的回答。

    我是愚蠢的,你是对的,它可以这样做,所以程序员不需要提示。我长篇大论的最后一部分(我想知道是否需要将其分为更多问题):正如Jon所说,lock充当了一个完整的屏障。原子操作也是如此(这可能解释了为什么它们不需要volatile参数)。

    0
    0 Comments

    volatile变量有用吗?如果有用,那么在什么情况下有用?

    是的。C#团队不会添加一个无用的特性。

    volatile变量在某些高度性能敏感的多线程应用程序中非常有用,这些应用程序的架构是基于在线程之间共享内存的。

    作为编辑的补充,我注意到普通的C#程序员很少会处于这些情况之一。首先,我们在这里讨论的性能特征大约在几十纳秒的数量级上;大多数业务应用程序的性能要求是以秒或分钟为单位的,而不是以纳秒为单位的。其次,大多数业务C#应用程序可以只使用少量线程来完成工作。第三,共享内存是一个坏主意,也是许多错误的原因;使用工作线程的业务应用程序不应该直接使用线程,而应该使用任务并行库(Task Parallel Library)安全地指示工作线程执行计算,然后返回结果。考虑使用C# 5.0中的新的await关键字来促进基于任务的异步,而不是直接使用线程。

    在业务应用程序中使用volatile变量是一个很大的警告信号,应该由专家进行深入审查,并且最好用更高级别、更不危险的实践来替代。

    锁将阻止指令重排序。

    锁在C#规范中被描述为代码中的一个特殊点,这样可以保证某些特殊的副作用在进入和退出锁时以特定的方式进行排序。

    volatile因为它会强制CPU总是从内存中读取值(然后不同的CPU/核心不会缓存它,也不会看到旧的值)。

    你所描述的是volatile如何实现的实现细节;没有要求volatile通过放弃缓存并返回到主存来实现。volatile的要求在规范中有详细说明。

    原子操作在单个原子(快速)操作中执行更改+赋值。

    我不明白你在“快速”后面加括号的原因;“快速”不是“原子”的同义词。

    锁如何防止缓存问题?

    再说一遍:锁被记录为代码中的一个特殊事件;编译器需要确保其他特殊事件与锁有一定的顺序。编译器选择如何实现这些语义是一个实现细节。

    在实践中,是的,锁引入了一个完整的屏障。

    在关键段中是否隐含了内存屏障?

    实践中是的,锁引入了一个完整的屏障。

    volatile变量不能是局部变量。

    正确。如果你从两个线程访问一个局部变量,那么这个局部变量必须是一个特殊的局部变量:它可以是委托的闭包外部变量,也可以是异步块中的变量,或者是迭代块中的变量。在所有情况下,这个局部变量实际上是一个字段。如果你想让这样的东西是volatile的,那么不要使用匿名方法、异步块或迭代块等高级特性!编写自己的闭包类,并根据需要使字段成为volatile。

    我从Eric Lippert的文章中读到了一些关于这个问题的内容,但现在我找不到那篇文章了,我也记不清他的回答了。

    好吧,我也记不清了,所以我在搜索引擎中输入了"Eric Lippert Why can't a local variable be volatile"。这让我找到了这个问题:why can't a local variable be volatile in C#?也许这就是你在想的问题。

    这让我想到它们不是通过Interlocked.CompareExchange()等来实现的。

    C#将volatile字段实现为volatile字段。volatile字段是CLR中的一个基本概念;CLR如何实现它们是CLR的一个实现细节。

    它们有什么不同?

    我不明白这个问题。

    例如,在这段代码中,volatile修饰符会有什么作用?

    ++_volatileField;

    它没有任何有用的作用,所以不要这样做。volatile和原子性是完全不同的东西。对一个volatile字段进行普通的递增操作不会使递增变成原子递增。

    此外,编译器(除了警告)在这里会做什么?

    如果被调用的方法引入了一个屏障,C#编译器应该抑制这个警告。我从来没有成功地把它加入到编译器中。希望团队有一天能够做到这一点。

    volatile字段将以原子方式更新。递增操作将引入一个屏障,因此跳过volatile的半屏障的事实得到了缓解。

    非volatile字段如何实现?

    这是CLR的实现细节。

    它们是否也会引入屏障?

    是的,原子操作引入屏障。同样,这是一个实现细节。

    与volatile相比,这不会对性能造成很大的损害吗?

    首先,将破损的代码与正常工作的代码进行比较是浪费时间。

    其次,如果你确实想浪费时间,你完全可以自己测量每种方法的性能。编写两种方式的代码,拿出一个秒表,每种方式运行一万亿次,你就会知道哪种方式更快。

    如果volatile不意味着屏障,但其他方式意味着,为什么我们不能在局部变量上使用它们?

    我甚至无法理解这个问题。

    +1 非常详尽!对于我不太清楚的部分...嗯,你在其他地方回答了。谢谢,我对这个话题有很多困惑。

    我从来没有真正看到引入volatile字段的语言/CLR特性的意义,因为有Thread.VolatileRead等,我认为这会导致代码更不容易出错。

    0