[C#.NET 拾遗补漏]10:理解 volatile 关键字(去除 Release 版本反向优化)

2023-01-17 IDE缓存

要理解 c# 中的 volatile 关键字,就要先知道编译器背后的一个基本优化原理。比如对于下面这段代码:

public class example
{
    public int x;
    public void dowork()
    {
        x = 5;
        var y = x + 10;
        debug.writeline("x = " +x + ", y = " +y);
    }
}

在 release 模式下,编译器读取 x = 5 后紧接着读取 y = x + 10,在单线程思维模式下,编译器会认为 y 的值始终都是 15。所以编译器会把 y = x + 10 优化为 y = 15,避免每次读取 y 都执行一次 x + 5。但 x 字段的值可能在运行时被其它的线程修改,我们拿到的 y 值并不是通过最新修改的 x 计算得来的,y 的值永远都是 15

也就是说,编译器在 release 模式下会对字段的访问进行优化,它假定字段都是由单个线程访问的,把与该字段相关的表达式运算结果编译成常量缓存起来,避免每次访问都重复运算。但这样就可能导致其它线程修改了字段值而当前线程却读取不到最新的字段值。为了防止编译器这么做,你就要让编译器用多线程思维去解读代码。告诉编译器字段的值可能会被其它线程修改,这种情况不要使用优化策略。而要做到这一点,就需要使用 volatile 关键字。

给类的字段添加 volatile 关键字,目的是告诉编译器该字段的值可能会被多个独立的线程改变,不要对该字段的访问进行优化。

使用 volatile 可以确保字段的值是可用的最新值,而且该值不会像非 volatile 字段值那样受到缓存的影响。好的做法是将每个可能被多个线程使用的字段标记为 volatile,以防止非预期的优化行为。

为了加深理解,我们来看一个实际的例子:

public class worker
{
    private bool _shouldstop;

    public void dowork()
    {
        bool work = false;
        // 注意:这里会被编译器优化为 while(true)
        while (!_shouldstop)
        {
            work = !work; // do sth.
        }
        console.writeline("工作线程:正在终止...");
    }

    public void requeststop()
    {
        _shouldstop = true;
    }
}

public class program
{
    public static void main()
    {
        var worker = new worker();

        console.writeline("主线程:启动工作线程...");
        var workertask = task.run(worker.dowork);

        // 等待 500 毫秒以确保工作线程已在执行
        thread.sleep(500);

        console.writeline("主线程:请求终止工作线程...");
        worker.requeststop();

        // 待待工作线程执行结束
        workertask.wait();
        //workerthread.join();

        console.writeline("主线程:工作线程已终止");
    }
}

在这个例子中,while (!_shouldstop) 会被编译器优化为 while(true)。我们可以看一下实际的运行效果来验证这一点。切换 release 模式,按 ctrl + f5 运行程序,运行效果始终如下:

程序运行后,虽然主线程在 500 毫秒后执行 requeststop() 方法修改了 _shouldstop 的值,但工作线程始终都获取不到 _shouldstop 最新的值,也就永远都不会终止 while 循环。

我们修改一下程序,对 _shouldstop 字段加上 volatile 关键字:

public class worker
{
    private volatile bool _shouldstop;

    public void dowork()
    {
        bool work = false;
        // 获取的是最新的 _shouldstop 值
        while (!_shouldstop)
        {
            work = !work; // do sth.
        }
        console.writeline("工作线程:正在终止...");
    }

    // ...(略)
}

此时在主线程调用 requeststop() 方法后,工作线程便立即终止了,运行效果如下图所示:

这说明加了 volatile 关键字后,程序可以实时读取到字段的最新值。

注意,一定要切换为 release 模式运行才能看到 volatile 发挥的作用,debug 模式下即使添加了 volatile 关键字,编译器也是不会执行优化的。

当然,并不是所有的类型都可以使用 volatile 关键字修饰的,常见的使用 volatile 的类型是这些简单类型:sbyte, byte, short, ushort, int, uint, char, float 和 bool,其它的请查看参考链接。

参考: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/volatile

上一篇:WPF MVVM 弹框之等待框

下一篇:[C#.NET 拾遗补漏]09:数据标注与数据校验