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

本文关键字:情况下 波动性 无限循环 变量 条件 程序 布尔 为什么 | 更新日期: 2023-09-27 18:19:29

我想了解何时需要将变量声明为volatile。为此,我写了一个小程序,并期望它进入无限循环,因为缺少条件变量的波动性。它并没有进入无限循环,并且在没有volatile关键字的情况下运行良好。

两个问题:

  1. 我应该在下面的代码列表中更改什么,以便它绝对需要使用volatile?

  2. 如果C#编译器发现一个变量是从另一个线程访问的,那么它是否足够聪明,可以将其视为易失性变量?

以上引发了我更多的问题:)

a。易失性只是一个暗示吗?

b。在多线程的上下文中,我什么时候应该将变量声明为volatile?

c。线程安全类的所有成员变量都应该声明为volatile吗?这太夸张了吗?

代码清单(重点是波动性而非线程安全):

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;
    }

谢谢。

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

首先,Joe Duffy说"易失性是邪恶的"——这对我来说已经足够好了

如果你真的想考虑易失性,你必须考虑内存围栏和优化——通过编译器、抖动和CPU。

在x86上,写入是释放围栏,这意味着后台线程将把true值刷新到内存中。

因此,您要查找的是循环谓词中false值的缓存。complexer或jitter可能会优化谓词并只对其求值一次,但我猜它对类字段的读取不会这样做。CPU不会缓存false值,因为您正在调用包含围栏的Console.WriteLine

此代码需要volatile,并且在没有Volatile.Read:的情况下永远不会终止

static void Run()
{
    bool stop = false;
    Task.Factory.StartNew( () => { Thread.Sleep( 1000 ); stop = true; } );
    while ( !stop ) ;
}

我不是C#并发方面的专家,但AFAIK您的期望是不正确的。从不同线程修改非易失性变量并不意味着其他线程永远看不到更改。只是无法保证何时(以及如果)发生。在你的情况下,它确实发生了(顺便问一下,你运行了多少次程序?),可能是因为最后一个线程根据@Russell的评论刷新了它的更改。但在现实生活中,涉及更复杂的程序流、更多的变量、更多的线程,的更新可能会在5秒后发生,或者——可能是千分之一的情况——可能根本不会发生。

因此,在不观察任何问题的情况下运行程序一次,甚至一百万次,只能提供统计数据,而不是绝对的证据。"没有证据就不是缺席的证据"。

试着这样重写:

    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
    }

并确保您在发布模式而不是调试模式下运行。在Release模式中,应用了优化,这实际上会导致代码在缺少volatile的情况下失败。

编辑:关于volatile:

我们都知道,程序生命周期中有两个不同的实体,它们可以以变量缓存和/或指令重新排序的形式应用优化:编译器和CPU。

这意味着,代码的编写方式和实际执行方式之间甚至可能存在很大差异,因为指令可能会相互重新排序,或者读取可能会缓存在编译器认为的"速度提高"中。

大多数情况下,这是好的,但有时(尤其是在多线程环境中),它可能会引起麻烦,如本例所示。为了让程序员能够手动防止这种优化,引入了内存栅栏,这是一种特殊的指令,其作用是防止指令相对于栅栏本身的重新排序(只读取、只写入或两者兼有),并强制CPU缓存中的值无效,因此每次都需要重新读取它们(这就是我们在上述场景中想要的)。

尽管您可以通过Thread.MemoryBarrier()指定一个影响所有变量的完整围栏,但如果您只需要一个变量受到影响,那么这几乎总是过分了。因此,为了使单个变量在线程中始终是最新的,可以使用volatile仅为该变量引入读/写围栏。

volatile关键字是向编译器发出的不要对此变量进行单线程优化的消息。这意味着该变量可以由多线程修改。这使得变量值在读取时最"新鲜"。

您粘贴在这里的代码是使用volatile关键字的一个很好的例子。这段代码在没有"volatile"关键字的情况下工作也就不足为奇了。然而,当更多的线程正在运行并且您对标志值执行更复杂的操作时,它的行为可能更不可预测。

您只在那些可以由几个线程修改的变量上声明volatile。我不知道C#中的具体情况,但我认为不能对那些通过读写操作(如增量)修改的变量使用volatile。Volatile在更改值时不使用锁。因此,在volatile上设置标志(如上所述)是可以的,增加变量是不可以的——然后应该使用同步/锁定机制。

当后台线程将true分配给成员变量时,会有一个释放围栏,该值会写入内存,其他处理器的缓存会更新或刷新该地址。

Console.WriteLine的函数调用是一个完整的内存围栏,它可能做任何事情(除了编译器优化)的语义将要求不缓存stop

但是,如果删除对Console.WriteLine的调用,我会发现该函数仍在停止。

我相信编译器在没有优化的情况下,不会缓存从全局内存计算出的任何内容。volatile关键字是一条指令,甚至不考虑将任何涉及变量的表达式缓存到编译器/JIT。

这个代码仍然暂停(至少对我来说,我正在使用Mono):

public void Start()
{
    stop = false;
    var thread = new Thread(() =>
    {
        while(true)
        {
            Thread.Sleep(50);
            stop = !stop;
        }
    });
    thread.Start();
    while ( !(stop ^ stop) );
}

这表明while语句并没有阻止缓存,因为这表明即使在同一个表达式语句中也没有缓存变量。

这种优化看起来对内存模型很敏感,内存模型依赖于平台,这意味着这将在JIT编译器中完成;它将没有时间(或智能)/查看/其他线程中变量的使用情况,并因此阻止缓存。

也许微软不相信程序员能够知道何时使用volatile,并决定剥夺他们的责任,然后Mono也紧随其后。