当前位置: 首页 > 知识库问答 >
问题:

C#中的“volatile”关键字是否仍然存在漏洞?

党博超
2023-03-14

Joe Albahari有一个很棒的多线程系列,这是必读的,任何做C#多线程的人都应该熟记于心。

然而,在第4部分中,他提到了volatile的问题:

请注意,应用volatile并不能阻止先写后读的交换,这可能会产生脑筋急转弯。Joe Duffy用下面的例子很好地说明了这个问题:如果Test1和Test2同时在不同的线程上运行,那么a和b的值都可能为0(尽管在x和y上都使用volatile)

然后是MSDN文档不正确的注释:

MSDN文档说明,使用volatile关键字可以确保字段中始终存在最新的值。这是不正确的,因为正如我们所看到的,写入后再读取可以重新排序。

我查看了MSDN文档,上一次更改是在2015年,但仍然列出:

volatile关键字表示一个字段可能被同时执行的多个线程修改。声明为volatile的字段不受假定由单个线程访问的编译器优化的影响。这样可以确保字段中始终存在最新的值。

现在,我仍然避免使用volatile,而是更详细地防止线程使用陈旧数据:

private int foo;
private object fooLock = new object();
public int Foo {
    get { lock(fooLock) return foo; }
    set { lock(fooLock) foo = value; }
}

关于多线程的部分是在2011年写的,这个论点今天仍然有效吗?还是应该不惜一切代价避免volatile,转而使用锁或全内存Geofence,以防止引入非常难以产生的bug,正如前面提到的,这些bug甚至依赖于运行它的CPU供应商?

共有3个答案

谷梁凌
2023-03-14

volatile是一种非常有限的保证。这意味着该变量不受假定从单个线程访问的编译器优化的影响。这意味着,如果从一个线程写入变量,然后从另一个线程读取,那么另一个线程肯定会有最新的值。如果没有volatile,编译器可能会假设单线程访问,例如将值保存在寄存器中,这会阻止其他处理器访问最新的值。

正如您提到的代码示例所示,它不能保护您不让不同块中的方法重新排序。实际上,volatile使每个人都能访问一个volatile变量。它不能保证这些访问组的原子性。

如果您只想确保您的属性具有最新的单个值,您应该能够只使用易失性

如果您试图像执行原子操作一样执行多个并行操作,问题就会出现。如果你必须强制几个操作在一起是原子的,你需要锁定整个操作。再次考虑这个例子,但是使用锁:

class DoLocksReallySaveYouHere
{
  int x, y;
  object xlock = new object(), ylock = new object();

  void Test1()        // Executed on one thread
  {
    lock(xlock) {x = 1;}
    lock(ylock) {int a = y;}
    ...
  }

  void Test2()        // Executed on another thread
  {
    lock(ylock) {y = 1;}
    lock(xlock) {int b = x;}
    ...
  }
}

锁定原因可能会导致一些同步,这可能会阻止ab的值为0(我还没有测试过这个)。然而,由于xy都是独立锁定的,因此ab仍然可以不确定地以0值结束。

因此,在包装单个变量的修改的情况下,使用易失性应该是安全的,而使用lock并不会更安全。如果你需要原子地执行多个操作,你需要在整个原子块周围使用一个lock,否则调度仍然会导致非确定性行为。

谭献
2023-03-14

当我阅读MSDN文档时,我相信它是在说,如果在变量上看到volatile,就不必担心编译器优化会把值搞砸,因为它们会重新排序操作。这并不是说你可以避免自己的代码以错误的顺序在不同的线程上执行操作所导致的错误。(尽管无可否认,评论对此并不明确。)

盖夕
2023-03-14

尽管流行的博客文章声称这一点,但其当前实现中的Volatile并没有被破坏。然而,它的指定很糟糕,在字段上使用修饰符来指定内存顺序的想法也不是很好(将Java/C中的volatile与C的原子规范进行比较,后者有足够的时间从早期的错误中学习)。另一方面,MSDN的文章显然是由一个无权谈论并发性的人写的,完全是假的。。唯一明智的选择是完全忽略它。

Volatile保证在访问字段时获取/释放语义,并且只能应用于允许原子读写的类型。不多不少。这足以有效地实现许多无锁算法,例如非阻塞哈希映射。

一个非常简单的示例是使用易失性变量来发布数据。由于x上的不稳定性,以下代码段中的断言不能触发:

private int a;
private volatile bool x;

public void Publish()
{
    a = 1;
    x = true;
}

public void Read()
{
    if (x)
    {
        // if we observe x == true, we will always see the preceding write to a
        Debug.Assert(a == 1); 
    }
}

Volatile不易使用,在大多数情况下,最好使用更高级别的概念,但当性能很重要或要实现一些低级别的数据结构时,Volatile可能非常有用。

 类似资料:
  • 本文向大家介绍Java中的volatile关键字,包括了Java中的volatile关键字的使用技巧和注意事项,需要的朋友参考一下 volatile修饰符用于让JVM知道访问该变量的线程必须始终将其自身的变量私有副本与内存中的主副本合并。 访问易失性变量将同步所有在主存储器中缓存的变量副本。可变变量只能应用于对象类型或私有类型的实例变量。易失性对象引用可以为null。 示例

  • 一、Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的。 Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通

  • 我知道关于volatile有很多问题,但我只是被讨论搞糊涂了:Java:volatile如何保证这段代码中“数据”的可见性? 我读过的每个网站都说可以在缓存中存储一个变量(使这个值对于其他线程不可见),我甚至发现了这个例子https://dzone.com/articles/java-volatile-keyword-0 所以我的第一个问题是:Java是否在缓存中存储变量值(在哪个缓存中?l1 l

  • 我理解通知编译器值可能会被更改,但是为了完成这个功能,编译器是否需要引入一个内存栅栏来使其工作呢? 从我的理解来看,对易失性对象的操作顺序是不能重新排序的,必须保留。这似乎暗示了一些记忆栅栏是必要的,实际上没有办法绕过这一点。我这样说对吗? 在这个相关的问题上有一个有趣的讨论 乔纳森·韦克利写道: ...对不同volatile变量的访问不能由编译器重新排序,只要它们出现在单独的完整表达式中...没

  • 问题内容: 我阅读了一些有关该关键字的文章,但无法弄清其正确用法。您能否告诉我在C#和Java中应该使用什么? 问题答案: 对于C#和Java,“ volatile”告诉编译器一个变量的值一定不能被缓存,因为它的值可能会在程序本身范围之外改变。然后,如果变量“超出其控制范围”更改,编译器将避免可能导致问题的任何优化。

  • volatile的写操作,无法保证线程安全。例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值,在线程1对count进行修改之后,会write到主内存中,主内存中的count变量就会变为6;线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6;导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。