volatile变量有用吗?如果是的话那么?

时间:2013-12-03 10:33:35

标签: c# multithreading volatile

回答this question让我想到的事情对我来说仍然不明确。我们首先假设我们从this postthis post读取所有内容。

[开始编辑] 也许它不是那么明显(意大利幽默?!)但标题只是非常挑衅:当然应该有一个原因{{1已经包含在C#中,我只是无法理解它。 [end edit]

简而言之,我们知道我们有三种工具可以在线程之间共享变量:

  • volatile因为这会阻止指令重新排序。
  • lock因为会强制CPU始终从内存中读取值(然后不同的CPU /内核不会缓存它们并且它们将看不到旧值。)
  • 互锁操作(volatile / IncrementDecrement)因为他们将在单个原子中执行更改+分配( fast 比例如,一个易失性+锁定)操作。

我不明白(C#规格参考将不胜感激):

  • 锁定如何阻止缓存问题?它是否隐含了关键部分的记忆障碍?
  • 挥发性变量不能是本地的(我从Eric Lippert那里读到了一些关于此的内容,但我现在找不到那篇文章而且我不记得他的评论了 - 说实话 - 我甚至都不理解它好)。这让我觉得他们没有与CompareExchange和朋友一起实施,他们有什么不同?

此代码中Interlocked.CompareExchange()修饰符的作用是什么?

volatile

[开始编辑] 上一个例子涉及原子读取+写入,让我们将其改为volatile int _volatileField = 0; int _normalField = 0; void test() { Interlocked.Increment(ref _normalField); ++_volatileField; } ,这里我不是在讨论原子操作。 [end edit]

此外,这里将使用什么编译器(除警告之外):

_volatileField = 1;

它们似乎是完全不同的东西(正如我想象的那样)但是对于我的理解Interlocked.Increment(ref _volatileField); 操作数应该隐含地是易变的(然后它将仅添加原子增量)。非易失性领域怎么可能?他们也暗示了障碍吗?这不会对性能造成太大影响(与易变性相比)吗?

<击> 如果Interlocked.Increment()并不意味着障碍,而其他人则不然,那么为什么我们不能将它们用作局部变量?特别是当在例如并行循环中使用时,这将以显着的方式损害性能(我正在考虑具有很少代码的小函数,这些代码适用于可以很好地使用数据高速缓存的大量数据)。

[开始编辑] 我发现以前的句子真的不清楚(抱歉我的英文)。我的意思是:如果性能(volatilevolatile相比,比较适用)更好(是的,我们可以衡量,在某些情况下差异是可衡量的和可见的)那么为什么我们不能将它们用于局部变量?我正在考虑操纵大量数据的并行循环(其中开销和障碍可能会严重影响性能)。 [end edit]

3 个答案:

答案 0 :(得分:22)

这个问题非常令人困惑。让我试着把它分解。

  

volatile变量是否有用?

是。 C#团队不会添加无用的功能。

  

如果是,那么什么时候?

易失性变量在某些对性能敏感的多线程应用程序中非常有用,其中应用程序体系结构基于跨线程共享内存。

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

在LOB应用程序中使用volatile是一个很大的危险信号,应该由专家进行大量审查,理想情况下应该采用更高级别,更少危险的做法。

  

lock会阻止指令重新排序。

C#规范将锁描述为代码中的特殊点,以保证在进入和离开锁时以特定方式对某些特殊副作用进行排序。

  

volatile因为会强制CPU始终从内存中读取值(然后不同的CPU /内核不会缓存它并且它们不会看到旧值)。

您所描述的是有关如何实施易失性的实施细节;没有要求通过放弃缓存并返回主内存来实现volatile。挥发性的要求在规范中有详细说明。

  

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

我不清楚为什么你有括号和快速&#34;之后&#34;原子&#34 ;; &#34;快速&#34;不是&#34; atomic&#34;的同义词。

  

锁如何防止缓存问题?

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

  

是否隐含了关键部分的内存障碍?

在实践中是的,锁会引入一个完整的围栏。

  

易变量变量不能是本地的

正确。如果从两个线程访问本地,则本地必须是特殊的本地:它可以是委托的封闭外部变量,也可以是异步块或迭代器块。在所有情况下,本地实际上都被视为一个领域。如果你想要这样的东西是易变的,那么不要使用像匿名方法,异步块或迭代器块这样的高级功能!这就是混合最高级别和最低级别的C#编码,这是一件非常奇怪的事情。编写自己的闭包类,并根据需要使字段变为不稳定。

  

我从Eric Lippert那里读到了一些关于此事的内容,但我现在无法找到该帖子,而且我不记得他的答案。

好吧,我也不记得了,所以我输入了#34; Eric Lippert为什么局部变量不能变化?&#34;进入搜索引擎。这让我想到了这个问题:

why can't a local variable be volatile in C#?

也许这就是你的想法。

  

这让我觉得他们没有使用Interlocked.CompareExchange()和朋友实现。

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

  

他们与众不同?

我不明白这个问题。

  

此代码中的volatile修饰符会做什么?

++_volatileField;

它没有任何帮助,所以不要这样做。波动性和原子性是完全不同的东西。在volatile字段上执行正常增量不会使增量成为原子增量。

  

此外,这里将使用什么编译器(除警告之外):

如果被调用的方法引入了一个围栏,那么C#编译器真的应该禁止该警告。我从来没有设法进入编译器。希望有一天团队能够。

volatile字段将以原子方式更新。围栏将通过增量引入,因此可以减少跳过易失性半围栏的事实。

  

非易变场怎么可能?

这是CLR的实施细节。

  

它们是否也意味着障碍?

是的,互锁操作会带来障碍。同样,这是一个实现细节。

  

这不会影响性能(与易失性相比)吗?

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

其次,如果你想浪费时间,你完全有能力衡量每个人的表现。两种方式编写代码,拿出一个秒表,每次运行一万亿次,你就会知道哪个更快。

  

如果volatile并不意味着障碍,但其他人确实存在障碍,那么为什么我们不能将它们用作局部变量呢?

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

答案 1 :(得分:5)

对于以下代码,波动变量可能会非常有用:

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,因为编译器可以查看您是在修改本地,还是将对本地的引用传递给另一个方法。在这两种情况下,编译器都会假定值正在更改,并将禁用依赖于不更改值的优化。

但是,对于带有Lambdas等的C#的更高版本,事情并不那么明确。 See the reply from Eric Lippert in this thread.

答案 2 :(得分:2)

  

lock如何阻止缓存问题?它隐含了一个记忆障碍吗?   在关键部分?

是的,lock也充当了一个完整的范围(它具有获取和释放语义)。 Windows开发中心的This page解释了这是如何工作的:

  

处理器可以通过acquire支持内存屏障指令,   发布和fence语义。这些语义描述了顺序   哪种操作结果可用。随着获得   语义,操作结果可用之前   在代码中出现的任何操作的结果。随着发布   语义,操作结果可用后   在代码中出现在它之前的任何操作的结果。的栅栏   语义结合获取和释放语义。结果一个   使用fence语义的操作在任何之前都可用   在代码之后和之后出现的操作   出现在它之前的操作。


  

易失性变量不能是本地的(我从Eric Lippert那里读到了一些东西   关于这一点,但我现在找不到那个帖子,我不记得他了   回答)。这让我觉得它们没有实现   Interlocked.CompareExchange()和朋友,他们有什么不同?

     

此代码中的volatile修饰符会做什么?

它们的不同之处在于它们不会阻止涉及在同一内存上运行的其他线程的竞争条件。涉及volatile字段的读/修改/存储将不是整个的原子,即使这三个步骤中的每一步都是(C#具有chosen to guarantee原子性,用于易失性读取和写入)。

示例代码上的volatile不会做太多。它将确保当用于递增_volatileField的读取/修改/存储序列开始时,读取将实际进入存储器而不是可能从处理器高速缓存中得到满足,但是如果存在竞争条件则它将完全没有帮助是同时写入字段的其他线程。