有人能很好地解释一下c#中的volatile关键字吗?它能解决哪些问题,不能解决哪些问题?在哪些情况下,它将节省我使用锁定?


当前回答

编译器有时会改变代码中语句的顺序来优化它。通常这在单线程环境中不是问题,但在多线程环境中可能是问题。请看下面的例子:

 private static int _flag = 0;
 private static int _value = 0;

 var t1 = Task.Run(() =>
 {
     _value = 10; /* compiler could switch these lines */
     _flag = 5;
 });

 var t2 = Task.Run(() =>
 {
     if (_flag == 5)
     {
         Console.WriteLine("Value: {0}", _value);
     }
 });

如果运行t1和t2,您将期望没有输出或结果为“Value: 10”。可能是编译器在t1函数内部切换了行。如果t2执行,可能是_flag值为5,而_value值为0。因此,预期的逻辑可能会被打破。

为了解决这个问题,你可以使用volatile关键字,你可以应用到字段。此语句禁用编译器优化,以便您可以强制代码中的正确顺序。

private static volatile int _flag = 0;

只有在真正需要时才应该使用volatile,因为它会禁用某些编译器优化,这会影响性能。它也不是所有。net语言都支持(Visual Basic不支持),因此它阻碍了语言互操作性。

其他回答

CLR喜欢优化指令,所以当你在代码中访问一个字段时,它可能并不总是访问该字段的当前值(它可能来自堆栈等)。将字段标记为volatile可以确保该指令可以访问该字段的当前值。当程序中的并发线程或操作系统中运行的其他代码可以修改该值(在非锁定场景中)时,这很有用。

您显然失去了一些优化,但它确实使代码更加简单。

多个线程可以访问一个变量。 最新的更新将在变量上

我认为没有比Eric Lippert更好的人来回答这个问题了(在原文中强调):

In C#, "volatile" means not only "make sure that the compiler and the jitter do not perform any code reordering or register caching optimizations on this variable". It also means "tell the processors to do whatever it is they need to do to ensure that I am reading the latest value, even if that means halting other processors and making them synchronize main memory with their caches". Actually, that last bit is a lie. The true semantics of volatile reads and writes are considerably more complex than I've outlined here; in fact they do not actually guarantee that every processor stops what it is doing and updates caches to/from main memory. Rather, they provide weaker guarantees about how memory accesses before and after reads and writes may be observed to be ordered with respect to each other. Certain operations such as creating a new thread, entering a lock, or using one of the Interlocked family of methods introduce stronger guarantees about observation of ordering. If you want more details, read sections 3.10 and 10.5.3 of the C# 4.0 specification. Frankly, I discourage you from ever making a volatile field. Volatile fields are a sign that you are doing something downright crazy: you're attempting to read and write the same value on two different threads without putting a lock in place. Locks guarantee that memory read or modified inside the lock is observed to be consistent, locks guarantee that only one thread accesses a given chunk of memory at a time, and so on. The number of situations in which a lock is too slow is very small, and the probability that you are going to get the code wrong because you don't understand the exact memory model is very large. I don't attempt to write any low-lock code except for the most trivial usages of Interlocked operations. I leave the usage of "volatile" to real experts.

欲进一步阅读,请参阅:

理解低锁技术在多线程应用中的影响 再见不稳定

只需查看volatile关键字的官方页面,您就可以看到典型用法的示例。

public class Worker
{
    public void DoWork()
    {
        bool work = false;
        while (!_shouldStop)
        {
            work = !work; // simulate some work
        }
        Console.WriteLine("Worker thread: terminating gracefully.");
    }
    public void RequestStop()
    {
        _shouldStop = true;
    }
    
    private volatile bool _shouldStop;
}

将volatile修饰符添加到_shouldStop的声明中,您将总是得到相同的结果。但是,如果_shouldStop成员上没有这个修饰符,行为是不可预测的。

所以这绝对不是完全疯狂的事情。

存在缓存一致性,负责CPU缓存的一致性。

如果CPU采用强内存模型(如x86)

因此,volatile字段的读写在x86上不需要特殊的指令:普通的读写(例如,使用MOV指令)就足够了。

示例来自c# 5.0规范(第10.5.3章)

using System;
using System.Threading;
class Test
{
    public static int result;   
    public static volatile bool finished;
    static void Thread2() {
        result = 143;    
        finished = true; 
    }
    static void Main() {

        finished = false;
        new Thread(new ThreadStart(Thread2)).Start();

        for (;;) {
            if (finished) {
                Console.WriteLine("result = {0}", result);
                return;
            }
        }
    }
}

产生输出:result = 143

如果finished字段没有被声明为volatile,那么在store to finished之后,允许主线程可以看到store to result,因此主线程可以从字段结果中读取值0。

Volatile行为依赖于平台,所以你应该在需要的时候考虑使用Volatile,以确保它能满足你的需求。

即使是volatile也不能阻止(所有类型的)重排序(c# - c#内存模型的理论与实践,第2部分)

尽管对A的写操作是不稳定的,从A_Won的读操作也是不稳定的,但栅栏都是单向的,实际上允许这种重新排序。

所以我相信,如果你想知道什么时候使用volatile (vs lock vs Interlocked),你应该熟悉内存围栏(full, half)和同步的需求。那为了你自己,你自己去找答案吧。

编译器有时会改变代码中语句的顺序来优化它。通常这在单线程环境中不是问题,但在多线程环境中可能是问题。请看下面的例子:

 private static int _flag = 0;
 private static int _value = 0;

 var t1 = Task.Run(() =>
 {
     _value = 10; /* compiler could switch these lines */
     _flag = 5;
 });

 var t2 = Task.Run(() =>
 {
     if (_flag == 5)
     {
         Console.WriteLine("Value: {0}", _value);
     }
 });

如果运行t1和t2,您将期望没有输出或结果为“Value: 10”。可能是编译器在t1函数内部切换了行。如果t2执行,可能是_flag值为5,而_value值为0。因此,预期的逻辑可能会被打破。

为了解决这个问题,你可以使用volatile关键字,你可以应用到字段。此语句禁用编译器优化,以便您可以强制代码中的正确顺序。

private static volatile int _flag = 0;

只有在真正需要时才应该使用volatile,因为它会禁用某些编译器优化,这会影响性能。它也不是所有。net语言都支持(Visual Basic不支持),因此它阻碍了语言互操作性。