C# volatile 使用详解

总目录


前言

在多线程编程中,确保线程之间的正确同步和可见性是一个关键挑战。C# 提供了多种机制来处理这些挑战,其中之一就是 volatile 关键字。它用于指示编译器和运行时环境不要对特定变量进行某些优化,以保证该变量的读写操作是线程安全的。


一、什么是 volatile?

1. 基础概念

  • volatile 关键字指示一个字段可以由多个同时执行的线程修改。
    • volatile关键字用于修饰字段(成员变量),向编译器和运行时表明该字段可能会被多个线程同时访问和修改,并且它的值可能随时发生变化。
  • 出于性能原因,编译器,运行时系统甚至硬件都可能重新排列对存储器位置的读取和写入。被volatile修饰的字段会禁止编译器和处理器对其执行指令重排序或缓存优化,确保该字段的每次读取都直接从内存中获取最新值,每次写入都立即刷新到内存中,避免因缓存或指令重排导致的数据不一致问题。

2. 主要特征

  • 禁止指令重排:编译器和处理器为了提高性能,可能会对指令进行重排序。在单线程环境下,指令重排不会影响程序的正确性,但在多线程环境中,可能会导致数据不一致。volatile关键字会禁止指令重排,确保对volatile字段的操作按照代码顺序执行。
  • 可见性保证:在多线程环境中,每个线程可能有自己的缓存,当线程访问变量时,可能会先从缓存中读取数据,而不是直接从主内存读取。如果一个线程修改了变量的值,其他线程的缓存可能不会立即更新,从而导致不同线程看到的变量值不一致。volatile关键字通过强制线程直接从主内存读取和写入数据,保证了数据的可见性。
  • 内存屏障:每次读写 volatile 变量都会插入适当的内存屏障(Memory Barrier),这阻止了其他线程看到过期的数据视图。

3. 支持的类型

volatile 可以修饰以下类型的字段:

  • 所有引用类型(如类、接口、数组等)
  • 指针(仅限不安全上下文)
  • 简单类型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool。
  • 具有以下基本类型之一的 enum 类型:byte、sbyte、short、ushort、int 或 uint。
  • 已知为引用类型的泛型类型参数。
  • IntPtr 和 UIntPtr。

其他类型(包括 double 和 long)无法标记为 volatile,因为对这些类型的字段的读取和写入不能保证是原子的。 若要保护对这些类型字段的多线程访问,请使用 Interlocked 类成员或使用 lock 语句保护访问权限。

volatile 关键字只能应用于 class 或 struct 的字段。 不能将局部变量声明为 volatile。

4. 使用示例

volatile关键字只能用于修饰字段,不能用于局部变量、方法参数或返回值等。以下是一个简单的示例:

class VolatileExample
{
    // 使用 volatile 修饰字段
    public volatile bool isRunning; 

    public void Start()
    {
        isRunning = true;
        while (isRunning)
        {
            // 执行一些操作
        }
    }

    public void Stop()
    {
        isRunning = false;
    }
}

在上述代码中,isRunning字段被volatile修饰,确保在Start方法的循环中,每次判断isRunning的值时,都会从主内存中读取最新值。当Stop方法修改isRunning的值为false时,Start方法能立即看到这个变化,从而退出循环。

二、编译器优化示例

该示例大部分内容来自:[C#.NET 拾遗补漏]10:理解 volatile 关键字

要理解 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;
        }
    }

    internal class Program
    {

        public static void Main()
        {
            Worker workerObject = new Worker();
            Thread workerThread = new Thread(workerObject.DoWork);

            workerThread.Start();
            Console.WriteLine("主线程:启动工作线程...");

            // 循环直到工作线程激活。
            while (!workerThread.IsAlive);

            // 让主线程休眠500毫秒,让工作线程做一些工作。
            Console.WriteLine("主线程:请求终止工作线程...");
            Thread.Sleep(500);

            // 请求工作线程自行停止。
            workerObject.RequestStop();

            // 等待线程执行完毕
            workerThread.Join();
            Console.WriteLine("主线程:工作线程已终止");
        }
    }

在Debug 模式下的运行结果:
在这里插入图片描述
在Release 模式下的运行结果:
在这里插入图片描述
产生这个问题的原因就在于:
在Release 模式下,while (!_shouldStop) 会被编译器 优化为 while(true) ,虽然主线程在500ms 后执行了RequestStop() 方法修改了 _shouldStop 的值,但工作线程始终都获取不到 _shouldStop 最新的值,也就永远都不会终止 while 循环。

如何解决呢?
解决办法就是上文介绍的 volatile ,对 _shouldStop 字段加上 volatile 关键字:

    public class Worker
    {
        private volatile bool _shouldStop;

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

        public void RequestStop()
        {
            _shouldStop = true;
        }
    }

    internal class Program
    {

        public static void Main()
        {
            Worker workerObject = new Worker();
            Thread workerThread = new Thread(workerObject.DoWork);

            workerThread.Start();
            Console.WriteLine("主线程:启动工作线程...");

            // 循环直到工作线程激活。
            while (!workerThread.IsAlive);

            // 让主线程休眠500毫秒,让工作线程做一些工作。
            Thread.Sleep(500);

            // 请求工作线程自行停止。
            Console.WriteLine("主线程:请求终止工作线程...");
            workerObject.RequestStop();

            // 等待线程执行完毕
            workerThread.Join();
            Console.WriteLine("主线程:工作线程已终止");
        }
    }

Release模式下 运行结果:
在这里插入图片描述

三、使用场景与示例

1. 标志位

适用于一个线程写、多个线程读的场景,且写操作是原子操作(如简单的赋值操作)。例如,使用 volatile 修饰一个标志位,一个线程负责修改这个标志位,其他线程根据这个标志位的值来决定是否执行某些操作。

private volatile bool _isRunning = true;

public void Stop()
{
    _isRunning = false;
}

public void DoWork()
{
    while (_isRunning)
    {
        // 执行一些工作...
    }
}

在这个例子中,_isRunning 被标记为 volatile,这样即使另一个线程调用了 Stop() 方法改变其值,当前线程也会立刻察觉到这个变化并停止循环。

2. 双重检查锁定(DCL)

为什么需要 volatile?
在多线程环境中,如果不使用 volatile,可能会遇到以下问题:

  • 指令重排:编译器或CPU可能会对指令进行优化重排,导致即使在加锁的情况下,也可能看到未完全构造好的对象引用。例如,JIT编译器可能先分配内存地址给 _instance,然后执行构造函数,但在某些平台上,这两个步骤可能被重新排序,使得其他线程在构造函数完成前就看到了非空的 _instance。
  • 缓存一致性:不同线程可能看到不同的缓存版本的数据,即一个线程更新了 _instance,但另一个线程由于读取的是本地缓存,仍然认为它是 null。

volatile 关键字可以解决上述两个问题,因为它:

  • 禁止指令重排,确保所有写操作都按照代码顺序发生。
  • 强制每次读取都从主内存获取最新值,而不是依赖于寄存器或CPU缓存中的旧数据。

在实现单例模式的双重检查锁定时,volatile关键字可以避免因指令重排导致的问题。以下是一个单例模式的示例:

public sealed class Singleton
{
    // 使用 volatile 修饰符确保线程安全
    private static volatile Singleton _instance;
    private static readonly object _lock = new object();

    private Singleton()
    {
        // 私有构造函数防止外部实例化
    }

    public static Singleton Instance
    {
        get
        {
            if (_instance == null) // 第一次检查
            {
                lock (_lock)
                {
                    if (_instance == null) // 第二次检查
                    {
                        _instance = new Singleton(); // 创建实例
                    }
                }
            }
            return _instance;
        }
    }
}

在这个例子中,_instance 被声明为 volatile :

  • 以确保在第一个 if 语句中读取 _instance 都会直接从主内存中获取最新值,避免了由于缓存不一致导致的问题。
  • 构造函数的执行不会与 _instance 的赋值操作重排,确保其他线程只能看到一个完全初始化的对象。
  • 虽然 lock 是一种强大的同步机制,它可以确保临界区内代码的线程安全,但在某些情况下,结合使用 volatile 可以为你的程序提供更多层次的保护和优化。特别是当你需要处理复杂的对象初始化、频繁的读取操作或者采用双检查锁定模式时,volatile 能够帮助你实现更高效且可靠的并发控制。

五、注意事项

  • Release 模式运行:注意,一定要切换为 Release 模式运行才能看到 volatile 发挥的作用,Debug 模式下即使添加了 volatile 关键字,编译器也是不会执行优化的。
  • 并非万能同步机制volatile关键字只能保证变量的可见性和一定程度上的有序性,但不能保证操作的原子性。 例如,对于volatile int sharedCounter; sharedCounter++;这样的操作,虽然每次读取和写入sharedCounter的值都是从主内存进行的,但sharedCounter++实际上包含了读取、加 1 和写入三个操作,不是原子操作,在多线程环境下仍可能出现数据竞争问题。如果需要原子操作,可以使用Interlocked类。
using System;
using System.Threading;

class Program
{
    private static volatile int sharedCounter = 0;

    static void Main()
    {
        // 创建两个线程
        Thread thread1 = new Thread(IncrementCounter);
        Thread thread2 = new Thread(IncrementCounter);

        // 启动线程
        thread1.Start();
        thread2.Start();

        // 等待两个线程执行完毕
        thread1.Join();
        thread2.Join();

        // 输出最终的计数器值
        Console.WriteLine($"Final counter value: {sharedCounter}");

		//第一次输出结果: Final counter value: 1226406
		//第二次输出结果: Final counter value: 1551244
		// ...
		// 会发现每次输出的结果都不一样
    }

    static void IncrementCounter()
    {
        for (int i = 0; i < 100_0000; i++)
        {
            sharedCounter++;
        }
    }
}

  • 性能影响:由于volatile关键字禁止了编译器和处理器的一些优化,频繁使用volatile可能会对性能产生一定的影响。因此,只有在确实需要保证变量的可见性时才使用volatile,避免滥用。
  • 与属性结合使用时需谨慎:volatile 只能修饰字段,不能直接应用于属性。如果需要对属性进行类似的保护,可以在内部实现中使用 volatile 字段。

结语

回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。


参考资料:
volatile(C# 参考)
[C#.NET 拾遗补漏]10:理解 volatile 关键字

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/959941.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于OSAL的嵌入式裸机事件驱动框架——整体架构调度机制

参考B站up主【架构分析】嵌入式祼机事件驱动框架 感谢大佬分享 任务ID &#xff1a; TASK_XXX TASK_XXX 在系统中每个任务的ID是唯一的&#xff0c;范围是 0 to 0xFFFE&#xff0c;0xFFFF保留为SYS_TSK_INIT。 同时任务ID的大小也充当任务调度的优先级&#xff0c;ID越大&#…

理解C++中的右值引用

右值引用&#xff0c;顾名思义&#xff0c;就是对一个右值进行引用&#xff0c;或者说给右值一个别名。右值引用的规则和左值一用一模一样&#xff0c;都是对一个值或者对象起个别名。 1. 右值引用和左值引用一样&#xff0c;在定义的同时必须立即赋值&#xff0c;如果不立即赋…

寒假1.23

题解 web&#xff1a;[极客大挑战 2019]Secret File&#xff08;文件包含漏洞&#xff09; 打开链接是一个普通的文字界面 查看一下源代码 发现一个链接&#xff0c;点进去看看 再点一次看看&#xff0c;没什么用 仔细看&#xff0c;有一个问题&#xff0c;当点击./action.ph…

分布式版本控制系统:Git

1 Git概述 Git官网&#xff1a;https://git-scm.com/ Git是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的各种项目Git易于学习&#xff0c;占地面积小&#xff0c;性能极快。它具有廉价的本地库、方便的暂存区域和多个工作流分支等特性…

< OS 有关 > 阿里云:轻量应用服务器 的使用 :轻量化 阿里云 vpm 主机

原因&#xff1a; &#xff1c; OS 有关 &#xff1e; 阿里云&#xff1a;轻量应用服务器 的使用 &#xff1a;从新开始 配置 SSH 主机名 DNS Tailscale 更新OS安装包 最主要是 清除阿里云客户端这个性能杀手-CSDN博客 防止 I/O 祸害系统 操作&#xff1a; 查看进程&#x…

工业相机 SDK 二次开发-Sherlock插件

本文介绍了 sherlock 连接相机时的插件使用。通过本套插件可连接海康的工业相机。 一&#xff0e;环境配置 1. 拷贝动态库 在用户安装 MVS 目录下按照如下路径 Development\ThirdPartyPlatformAdapter 找到目 录为 DalsaSherlock 的文件夹&#xff0c;根据 Sherlock 版本找到…

为什么机器学习中梯度下降是减去斜率,而不是按照其数学意义减去斜率的倒数

做个简单假设&#xff0c;Loss函数的某一个参数的函数曲线是二次方程&#xff0c;其导数函数为 r 2 ∗ w r 2*w r2∗w 按照斜率意义来看&#xff0c;要减去斜率倒数 降低LOSS需要将w1更新为w2&#xff0c;所以更新公式为 w w − Δ L Δ w w w - \frac{\Delta L}{\Delta w…

Linux高级--3.3.2 自定义协议设计--ProtoBuf

一、自定义协议设计的必要性 自定义通信协议通常有以下几个原因&#xff0c;尤其在IM即时通信、节点服务器、HTTP协议、Nginx、Redis协议、SOME/IP协议和DoIP协议等场景中&#xff0c;设计和使用自定义协议能带来特定的优势&#xff1a; 1. 性能优化 更高效的资源利用&#…

技术总结:FPGA基于GTX+RIFFA架构实现多功能SDI视频转PCIE采集卡设计方案

目录 1、前言工程概述免责声明 3、详细设计方案设计框图SDI 输入设备Gv8601a 均衡器GTX 解串与串化SMPTE SD/HD/3G SDI IP核BT1120转RGBFDMA图像缓存RIFFA用户数据控制RIFFA架构详解Xilinx 7 Series Integrated Block for PCI ExpressRIFFA驱动及其安装QT上位机HDMI输出RGB转BT…

docker 部署 java 项目详解

在平常的开发工作中&#xff0c;我们经常需要部署项目&#xff0c;开发测试完成后&#xff0c;最关键的一步就是部署。今天我们以若依项目为例&#xff0c;总结下部署项目的整体流程。简单来说&#xff0c;第一步&#xff1a;安装项目所需的中间件&#xff1b;第二步&#xff1…

动手学图神经网络(2):跆拳道俱乐部案例实战

动手学图神经网络&#xff08;2&#xff09;&#xff1a;跆拳道俱乐部案例实战 在深度学习领域&#xff0c;图神经网络&#xff08;GNNs&#xff09;能将传统深度学习概念推广到不规则的图结构数据&#xff0c;使神经网络能够处理对象及其关系。将基于 PyTorch Geometric 库&a…

Elastic Agent 对 Kafka 的新输出:数据收集和流式传输的无限可能性

作者&#xff1a;来 Elastic Valerio Arvizzigno, Geetha Anne 及 Jeremy Hogan 介绍 Elastic Agent 的新功能&#xff1a;原生输出到 Kafka。借助这一最新功能&#xff0c;Elastic 用户现在可以轻松地将数据路由到 Kafka 集群&#xff0c;从而实现数据流和处理中无与伦比的可扩…

1.25学习

web bugku-源代码 打开环境后看到了一个提交的界面&#xff0c;我们根据题目查看源代码&#xff0c;看到了js代码&#xff0c;其中有几处是url编码&#xff0c;我们对其进行解码&#xff0c;后面的unescape&#xff08;&#xff09;函数就是将p1解码以及%35%34%61%61%32p2解码…

Hive详细讲解-基础语法快速入门

文章目录 1.DDL数据库相关操作1.1创建数据库1.2指定路径下创建数据库1.3添加额外信息创建with dbproperties1.4查看数据库 结合like模糊查询 2.查看某一个数据库的相关信息2.1.如何查看数据库信息&#xff0c;extended可选2.2修改数据库 3.Hive基本数据类型4.复杂数据类型5.类型…

深度解析:基于Vue 3与Element Plus的学校管理系统技术实现

一、项目架构分析 1.1 技术栈全景 核心框架&#xff1a;Vue 3 TypeScript UI组件库&#xff1a;Element Plus&#xff08;含图标动态注册&#xff09; 状态管理&#xff1a;Pinia&#xff08;用户状态持久化&#xff09; 路由方案&#xff1a;Vue Router&#xff08;动态路…

基于Django的个人博客系统的设计与实现

【Django】基于Django的个人博客系统的设计与实现&#xff08;完整系统源码开发笔记详细部署教程&#xff09;✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 系统采用Python作为主要开发语言&#xff0c;结合Django框架构建后端逻辑&#xff0c;并运用J…

【架构面试】一、架构设计认知

涉及分布式锁、中间件、数据库、分布式缓存、系统高可用等多个技术领域&#xff0c;旨在考查候选人的技术深度、架构设计能力与解决实际问题的能力。 1. 以 Redis 是否可以作为分布式锁为例&#xff1a; 用 Redis 实现分布式锁会存在哪些问题&#xff1f; 死锁&#xff1a;如果…

DrawDB:超好用的,免费数据库设计工具

DrawDB&#xff1a;超好用的&#xff0c;免费数据库设计工具 引言 在软件开发过程中&#xff0c;数据库设计是一个至关重要的环节。 无论是关系型数据库还是非关系型数据库&#xff0c;良好的数据库设计都能显著提升系统的性能和可维护性。 然而&#xff0c;数据库设计往往…

如何将xps文件转换为txt文件?xps转为pdf,pdf转为txt,提取pdf表格并转为txt

文章目录 xps转txt方法一方法二 pdf转txt整页转txt提取pdf表格&#xff0c;并转为txt 总结另外参考XPS文件转换为TXT文件XPS文件转换为PDF文件PDF文件转换为TXT文件提取PDF表格并转为TXT示例代码&#xff08;部分&#xff09; 本文测试代码已上传&#xff0c;路径如下&#xff…

【Linux】线程、线程控制、地址空间布局

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;Linux 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 1、Linux线程1.1 线程的优缺点1.2 线程异常和用途1.3 线程等待1.3 线程终止1.4 线程分离1.5 线程ID和地址空间布局1.6 线程栈 1、…