“volatile”关键字在C#中是否仍然存在问题?

25 浏览
0 Comments

“volatile”关键字在C#中是否仍然存在问题?

Joe Albahari在多线程方面有一系列必读的文章,任何进行C#多线程编程的人都应该熟记于心。然而,在第四部分中,他提到了使用volatile关键字时的问题:

“需要注意的是,应用volatile关键字并不能阻止写入后紧跟着读取的交换,这可能会导致一些难以理解的问题。Joe Duffy通过以下示例很好地说明了这个问题:如果Test1和Test2在不同线程上同时运行,那么a和b都有可能最终都为0(尽管在x和y上都使用了volatile关键字)。”

然后是对MSDN文档的错误说明:

“MSDN文档声称使用volatile关键字可以确保字段中始终存在最新的值。这是不正确的,因为如我们所见,写入后紧跟着读取可能会被重新排序。”

我查看了最后一次更改于2015年的MSDN文档,但仍然列出了以下内容:

“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,而选择使用锁定或完整内存屏障,尤其是考虑到这取决于它所运行的CPU供应商?

0
0 Comments

在C#中,关键字`volatile`用于标记一个变量,表示该变量可能会被多个线程同时访问。根据MSDN文档的解释,使用`volatile`关键字可以防止编译器对变量进行优化,从而确保在所有时间点上都能获取到最新的值。

然而,这并不意味着使用`volatile`关键字就能保证线程安全。上述评论中提到,如果多个线程按照错误的顺序执行操作,仍然会产生错误。因此,`volatile`关键字只是针对编译器优化的一种手段,并不能解决所有线程安全的问题。

此外,还有评论指出,CPU的重排序也会导致线程安全问题。每个编译器都必须了解CPU的重排序规则。如果CPU可以对指令A和B进行重排序,但语言语义要求不能这样做,那么编译器必须引入某种方式来阻止重排序,或选择其他指令来达到相同的目标。因此,编译器不仅需要避免自身的重排序,还需要防止CPU的重排序。

出现`volatile`关键字仍然无法解决线程安全问题的原因是,它只是针对编译器优化的一种手段,无法防止线程按照错误的顺序执行操作,也无法防止CPU的重排序。为了解决线程安全问题,需要更加细致的控制和管理多线程访问共享变量的顺序和操作。

解决线程安全问题的方法包括使用锁机制、使用线程安全的数据结构或使用更高级别的并发编程模型,如并发集合等。

总之,`volatile`关键字只是保证变量在多线程环境下的可见性,并不能解决所有的线程安全问题。在编写多线程代码时,需要综合考虑编译器优化、CPU重排序以及多个线程间的操作顺序,以确保代码的正确性和线程安全性。

0
0 Comments

volatile关键字在C#中是否仍然存在问题?

volatile关键字提供了一个非常有限的保证。它意味着变量不受编译器优化的影响,这些优化假设变量只会被单个线程访问。这意味着如果你在一个线程中写入一个变量,然后在另一个线程中读取它,那么另一个线程一定会得到最新的值。如果没有volatile关键字,在一个多处理器的机器上,编译器可能会做出关于单线程访问的假设,例如通过将值保存在寄存器中,从而阻止其他处理器访问最新的值。

正如你提到的代码示例所显示的,它不能防止不同块中的方法重新排序。实际上,volatile关键字使得对volatile变量的每个单独访问都是原子的。它不能保证对这些访问组的原子性。

如果你只是想确保你的属性具有最新的单个值,你应该可以直接使用volatile关键字。

问题出现在你尝试将多个并行操作视为原子操作时。如果你需要强制多个操作一起具有原子性,你需要锁定整个操作。再考虑一下例子,但使用锁定:

class DoLocksReallySaveYouHere

{

int x, y;

object xlock = new object(), ylock = new object();

void Test1() // 在一个线程上执行

{

lock(xlock) {x = 1;}

lock(ylock) {int a = y;}

...

}

void Test2() // 在另一个线程上执行

{

lock(ylock) {y = 1;}

lock(xlock) {int b = x;}

...

}

}

锁定可能会引起一些同步,这可能会阻止a和b都具有值0(我没有测试过)。然而,由于x和y都是独立锁定的,所以a或b仍然可能以0的值非确定性地结束。

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

“基本上意味着变量不会被缓存”...叹气,又一个人在传播这个谬论 🙁 不,volatile并不保证这一点,事实上,在x86和其他体系结构上,volatile字段是可以被缓存的。而且,volatile绝对限制了内存访问的重排序,这对于使其有用是必不可少的。即使有一个合理的“不缓存”的定义(实际上没有),对于绝对每个算法来说,这也是完全无用的。

只是说我所学到的,抱歉。这个更正确吗?:“它意味着变量不受编译器优化的影响,这些优化假设变量只会被单个线程访问。...如果没有volatile,在没有volatile的多处理器机器上,编译器可能会做出关于单线程访问的假设,例如通过将值保存在寄存器中,从而阻止其他处理器访问最新的值。”

这是正确的,但是缺少volatile关键字的关键部分。volatile保证读写的获取/释放语义。例如,你不能将写入操作重新排序到volatile写入之后(但是可以将其重新排序到volatile写入之前)。如果你想开始理解整个过程,你可以从这里开始:这里。这是关于JMM的,但CLR MM在实践中非常相似。

0
0 Comments

C#中的'volatile'关键字是否仍然存在问题?

在当前的实现中,尽管有一些流行的博客声称'volatile'关键字存在问题,但它实际上并没有出现问题。然而,它的规范说明比较糟糕,并且在字段上使用修饰符来指定内存顺序的想法并不是很好(将Java/C#中的'volatile'与C++的原子规范进行比较,后者有足够的时间从早期的错误中吸取教训)。而另一方面,MSDN的文章明显是由一个对并发性毫无了解的人编写的,完全是虚假的...唯一明智的选择是完全忽略它。

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

一个非常简单的示例是使用'volatile'变量发布数据。由于x上有'volatile'修饰符,在下面的代码片段中的断言将不会触发:

private int a;
private volatile bool x;
public void Publish()
{
    a = 1;
    x = true;
}
public void Read()
{
    if (x)
    {
        // 如果我们观察到x == true,我们将始终看到对a的前面写入
        Debug.Assert(a == 1); 
    }
}

'volatile'并不容易使用,在大多数情况下,你最好使用一些更高级的概念。但是当性能很重要或者正在实现一些低级别的数据结构时,'volatile'会非常有用。

你是否打算将"a"设置为'volatile',或者'x'上的'volatile'保证了对"a"的写入已经发生?

代码是正确的。内存顺序保证要比"写入不能被缓存"严格得多,这在这里也起作用。简单来说:如果线程B看到了对'volatile'变量X的更新,那么它保证在线程A写入X的值之前看到所有的写入。这使我们可以使用一个单独的'volatile'布尔变量来发布其他数据。

'volatile'是否只具有获取/释放语义?还是顺序一致性?我原以为是后者,而这两者并不相同。

C#规范确实保证了获取/释放语义,并且不保证顺序一致性(规范的第10.5.3节)。顺序一致性是一种昂贵的保证,所以他们没有提供它。

哇,我不知道。谢谢!

"volatile保证原子性" - 这是错误的。'volatile'与原子性无关(参见ECMA-335规范)。

是的,确实是搞混了与JMM相关的东西。不过它确实保证了只能将'volatile'应用于保证是原子的类型,但应该更好地表述。

0