简体   繁体   English

为什么这个程序在没有布尔条件变量的波动性的情况下不会进入无限循环?

[英]Why this program does not go into infinite loop in absence of volatility of a boolean condition variable?

I wanted to understand on when exactly I need to declare a variable as volatile. 我想知道我何时需要将变量声明为volatile。 For that I wrote a small program and was expecting it to go into infinite loop because of missing volatility of a condition variable. 为此我写了一个小程序,并期望它因为缺少条件变量的波动而进入无限循环。 It did not went into infinite loop and worked fine without volatile keyword. 没有volatile关键字,它没有进入无限循环并且工作正常。

Two questions: 两个问题:

  1. What should I change in the below code listing - so that it absolutely requires use of volatile? 我应该在下面的代码清单中更改什么 - 这样它绝对需要使用volatile?

  2. Is C# compiler smart enough to treat a variable as volatile - if it sees that a variable is being accessed from a different thread? C#编译器是否足够智能将变量视为volatile - 如果它看到从另一个线程访问变量?

The above triggered more questions to me :) 以上引发了更多问题给我:)

a. 一种。 Is volatile just a hint? 易变只是一个暗示?

b. When should I declare a variable as volatile in context of multithreading? 什么时候应该在多线程的上下文中将变量声明为volatile?

c. C。 Should all member variables be declared volatile for a thread safe class? 是否应该为线程安全类声明所有成员变量volatile? Is that overkill? 那有点矫枉过正吗?

Code Listing (Volatility and not thread safety is the focus): 代码清单(波动性而非线程安全性是关注点):

class Program
{
    static void Main(string[] args)
    {
        VolatileDemo demo = new VolatileDemo();
        demo.Start();

        Console.WriteLine("Completed");
        Console.Read();
    }
}

    public class VolatileDemo
    {
        public VolatileDemo()
        {
        }

        public void Start()
        {
            var thread = new Thread(() =>
            {
                Thread.Sleep(5000);
                stop = true;
            });

            thread.Start();

            while (stop == false)
                Console.WriteLine("Waiting For Stop Event");
        }

        private bool stop = false;
    }

Thanks. 谢谢。

Firstly, Joe Duffy says "volatile is evil" - that's good enough for me. 首先, Joe Duffy说“挥发性是邪恶的” - 这对我来说已经足够了。

If you do want to think about volatile, you must think in terms of memory fences and optimisations - by the compiler, jitter and CPU. 如果你想考虑volatile,你必须考虑内存栅栏和优化 - 编译器,抖动和CPU。

On x86, writes are release fences, which means your background thread will flush the true value to memory. 在x86上,写入是释放围栏,这意味着您的后台线程将刷新true值到内存。

So, what you are looking for is a caching of the false value in your loop predicate. 因此,您正在寻找的是在循环谓词中缓存false值。 The complier or jitter may optimise the predicate and only evaluate it once, but I guess it doesn't do that for a read of a class field. 编译器或抖动可以优化谓词并且只评估它一次,但我想它不会对读取类字段这样做。 The CPU will not cache the false value because you are calling Console.WriteLine which includes a fence. CPU不会缓存false值,因为您正在调用包含fence的Console.WriteLine

This code requires volatile and will never terminate without a Volatile.Read : 此代码需要volatile并且永远不会在没有Volatile.Read情况下终止:

static void Run()
{
    bool stop = false;

    Task.Factory.StartNew( () => { Thread.Sleep( 1000 ); stop = true; } );

    while ( !stop ) ;
}

I am not an expert in C# concurrency, but AFAIK your expectation is incorrect. 我不是C#并发专家,但AFAIK你的期望是不正确的。 Modifying a non-volatile variable from a different thread does not mean that the change will never become visible to other threads. 从不同的线程修改非易失性变量并不意味着更改永远不会对其他线程可见。 Only that there is no guarantee when (and if) it happens . 只有当它(如果)发生时无法保证 In your case it did happen (how many times did you run the program btw?), possibly due to the finishing thread flushing its changes as per @Russell's comment. 在你的情况下它确实发生了(你运行程序btw多少次?),可能是由于完成线程按照@Russell的评论来刷新它的变化。 But in a real life setup - involving more complex program flow, more variables, more threads - the update may happen later than 5 seconds, or - maybe once in a thousand cases - may not happen at all. 但是在现实生活设置中 - 涉及更复杂的程序流,更多变量,更多线程 - 更新可能在5秒之后发生,或者 - 可能在一千个案例中一次 - 可能根本不会发生。

So running your program once - or even a million times - while not observing any problems only provides statistical, not absolute proof. 因此,运行程序一次 - 甚至一百万次 - 而不是观察任何问题只能提供统计,而不是绝对的证据。 "Absence of evidence is not evidence of absence" . “缺乏证据不是缺席的证据”

Try to rewrite it like this: 尝试像这样重写它:

    public void Start()
    {
        var thread = new Thread(() =>
        {
            Thread.Sleep(5000);
            stop = true;
        });

        thread.Start();

        bool unused = false;
        while (stop == false)
            unused = !unused; // fake work to prevent optimization
    }

And make sure you are running in Release mode and not Debug mode. 并确保您在发布模式而不是调试模式下运行。 In Release mode optimizations are applied which actually cause the code to fail in the absence of volatile . 在发布模式下,应用优化会导致代码在没有volatile的情况下失败。

Edit : A bit about volatile : 编辑 :有点volatile

We all know that there are two distinct entities involved in a program lifecycle that can apply optimizations in the form of variable caching and/or instruction reordering: the compiler and the CPU. 我们都知道程序生命周期中涉及两个不同的实体,它们可以以变量缓存和/或指令重新排序的形式应用优化:编译器和CPU。

This means that there may be even a large difference between how you wrote your code and how it actually gets executed, as instructions may be reordered with respect to eachother, or reads may be cached in what the compiler perceives as being an "improvement in speed". 这意味着您编写代码的方式与实际执行方式之间可能存在很大差异,因为指令可能会相互重新排序,或者读取可能会缓存在编译器认为是“速度提升”的内容中”。

Most of the times this is good, but sometimes (especially in the multithreading context) it can cause trouble as seen in this example. 大多数情况下这是好的,但有时(特别是在多线程上下文中)它可能会导致问题,如本例所示。 To allow the programmer to manually prevent such optimizations, memory fences were introduced, which are special instructions whose role is to prevent both reordering of instructions (just reads, just writes or both) with respect to the fence itself and also force the invalidation of values in CPU caches, such that they need to be re-read every time (which is what we want in the scenario above). 为了让程序员手动防止这种优化,引入了内存栅栏,它们是特殊指令,其作用是防止对栅栏本身重新排序指令(只读,只写或两者),并强制使值无效在CPU缓存中,每次都需要重新读取它们(这是我们在上面的场景中想要的)。

Although you can specify a full fence affecting all variables through Thread.MemoryBarrier() , it's almost always an overkill if you need only one variable to be affected. 虽然您可以通过Thread.MemoryBarrier()指定影响所有变量的完整栅栏,但如果只需要影响一个变量,则几乎总是Thread.MemoryBarrier()过分。 Thus, for a single variable to be always up-to-date across threads, you can use volatile to introduce read/write fences for that variable only. 因此,要使单个变量始终保持最新的线程,您可以使用volatile仅为该变量引入读/写栅栏。

volatile keyword is a message to a compiler not to make single-thread optimizations on this variable. volatile关键字是编译器不向此变量进行单线程优化的消息。 It means that this variable may be modified by multi threads. 这意味着可以通过多线程修改此变量。 This makes the variable value the most 'fresh' while reading. 这使得变量值在阅读时最“新鲜”。

The piece of code you've pasted here is a good example to use volatile keyword. 您在此处粘贴的代码是使用volatile关键字的一个很好的示例。 It's not a surprise that this code works without 'volatile' keyword. 没有'volatile'关键字,这段代码的工作原理并不奇怪。 However it may behave more unpredictible when more threads are running and you perform more sophisticated actions on the flag value. 但是,当运行更多线程并且您对标志值执行更复杂的操作时,它可能会表现得更加不可预测。

You declare volatile only on those variables which can be modified by several threads. 只在那些可由多个线程修改的变量上声明volatile。 I don't know exactly how it is in C#, but I assume you can't use volatile on those variables which are modified by read-write actions (such as incrementation). 我不知道C#中到底是怎么回事,但我认为你不能对那些被读写操作(例如递增)修改的变量使用volatile。 Volatile doesn't use locks while changing the value. Volatile在更改值时不使用锁。 So setting the flag on volatile (like above) is OK, incrementing the variable is not OK - you should use synchronization/locking mechanism then. 因此在volatile上设置标志(如上所述)是可以的,增加变量是不行的 - 那么你应该使用同步/锁定机制。

When the background thread assigns true to the member variable there is a release fence and the value is written to memory and the other processor's cache is updated or flushed of that address. 当后台线程为成员变量赋值true时,有一个释放栏,该值被写入内存,另一个处理器的缓存被更新或刷新该地址。

The function call to Console.WriteLine is a full memory fence and its semantics of possibly doing anything (short of compiler optimisations) would require that stop not be cached. Console.WriteLine的函数调用是一个完整的内存栅栏,它可能做任何事情(缺少编译器优化)的语义都要求不要缓存stop

However if you remove the call to Console.WriteLine , I find that the function is still halting. 但是,如果删除对Console.WriteLine的调用,我发现该函数仍在暂停。

I believe that the compiler in the absence of optimisations the compiler does not cache anything calculated from global memory. 我相信编译器在没有优化的情况下编译器不会缓存从全局内存计算的任何内容。 The volatile keyword is then an instruction not to even think of caching any expression involving the variable to the compiler / JIT. 那么volatile关键字是一条甚至不想将任何涉及变量的表达式缓存到编译器/ JIT的指令。

This code still halts (at least for me, I am using Mono): 此代码仍然停止(至少对我来说,我使用的是Mono):

public void Start()
{
    stop = false;

    var thread = new Thread(() =>
    {
        while(true)
        {
            Thread.Sleep(50);
            stop = !stop;
        }
    });

    thread.Start();

    while ( !(stop ^ stop) );
}

This shows that it's not the while statement preventing caching, because this shows the variable not being cached even within the same expression statement. 这表明它不是阻止缓存的while语句,因为这表明即使在同一个表达式语句中也没有缓存变量。

This optimisation look sensitive to the memory model, which is platform dependent meaning this would be done in the JIT compiler; 这种优化看起来对内存模型很敏感,这取决于平台,这意味着可以在JIT编译器中完成。 which wouldn't have time (or intelligence) to /see/ the usage of the variable in the other thread and prevent caching for that reason. 没有时间(或智能)/看到/在另一个线程中使用变量,并因此阻止缓存。

Perhaps Microsoft doesn't believe programmers capable of knowing when to use volatile and decided to strip them of the responsibility, and then Mono followed suit. 也许微软并不相信程序员能够知道何时使用volatile并决定剥夺他们的责任,然后Mono也纷纷效仿。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM