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 关键字

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Unity 热更方案] 使用Addressable进行打包管理, 使用AssetBundle进行包的加载管理.70%跟练

在正常的开发过程中我们经常遇到一些关于热更的方案,有一些已有的方案供我们选择,但是实机情况往往不尽如人意,各有优缺点. 现在我们同样有一个热更的需求,但是要求打包简单,加载过程可查,防止出现一些资源和流程的问题. 下面介绍我在项目中使用的方案. 打包方面使用Addressabl…

Flink运行时架构

一、系统架构 1&#xff09;作业管理器&#xff08;JobManager&#xff09; JobManager是一个Flink集群中任务管理和调度的核心&#xff0c;是控制应用执行的主进程。也就是说&#xff0c;每个应用都应该被唯一的JobManager所控制执行。 JobManger又包含3个不同的组件。 &am…

在 Windows 11 中为 SMB 3.x 文件共享协议提供 RDMA 支持

注&#xff1a;机翻&#xff0c;未校。 Enable SMB Direct in Windows 11 在 Windows 11 中启用 SMB Direct Provides RDMA support for the SMB 3.x file sharing protocol 为 SMB 3.x 文件共享协议提供 RDMA 支持 Vigneshwaran Vijayakumar November 3, 2024 Last Updat…

用AI生成PPT,办公效率提升新方式

用AI生成PPT&#xff0c;办公效率提升新方式&#xff01;在快节奏的时代&#xff0c;如何优雅应对高效办公的挑战&#xff1f; 或许你也有这样的经历&#xff1a;开会前临时被要求制作PPT&#xff0c;一阵头大&#xff0c;却只能硬着头皮上。科技的发展为我们带来了更智能的解…

单片机-STM32 IIC通信(OLED屏幕)(十一)

一、屏幕的分类 1、LED屏幕&#xff1a; 由无数个发光的LED灯珠按照一定的顺序排列而成&#xff0c;当需要显示内容的时候&#xff0c;点亮相关的LED灯即可&#xff0c;市场占有率很高&#xff0c;主要是用于户外&#xff0c;广告屏幕&#xff0c;成本低。 LED屏是一种用发光…

ASP.NET Core 6.0 如何处理丢失的 Startup.cs 文件

介绍 .NET 6.0 已经发布&#xff0c;ASP.NET Core 6.0 也已发布。其中有不少变化让很多人感到困惑。例如&#xff0c;“谁动了我的奶酪”&#xff0c;它在哪里Startup.cs&#xff1f;在这篇文章中&#xff0c;我将深入研究这个问题&#xff0c;看看它移动到了哪里以及其他变化。…

【嵌入式开发】stm32 st-link 烧录

使用 ST-Link 烧录 STM32 的程序可以通过多种工具实现&#xff0c;例如 STM32CubeProgrammer、Keil、IAR、以及 OpenOCD。以下是通用的步骤说明&#xff1a; 准备工作 硬件准备 确保 ST-Link 调试器与 STM32 芯片引脚正确连接&#xff1a; SWDIO (SWD 数据线) 接至 STM32 的 SW…

仿 RabbitMQ 的消息队列3(实战项目)

七. 消息存储设计 上一篇博客已经将消息统计文件的读写代码实现了&#xff0c;下一步我们将实现创建队列文件和目录。 实现创建队列文件和目录 初始化 0\t0 这样的初始值. //创建队列对应的文件和目录&#xff1a;public void createQueueFile(String queueName) throws IO…

【STM32HAL-----GPIO】

1. 什么是GPIO&#xff1f;&#xff08;了解&#xff09; 2. STM32 GPIO简介 2.1. GPIO特点 2.2. GPIO电气特性 2.3. GPIO引脚分布图 IO引脚分布特点&#xff1a;按组存在、组数视芯片而定、每组最多16个IO引脚。 3. IO端口基本结构介绍 4. GPIO八种工作模式 4.1. 输入浮空 特…

Midjourney基础-常用修饰词+权重的用法大全

用好修饰词很关键 Midjourney要用除了掌握好提示词的写法&#xff0c;按照上一篇《做Midjourney最好图文教程-提示词公式以及高级参数讲解》画面主体 场景氛围 主体行为 构图方式 艺术风格 图像质量。 要画出有质感的内容我们必须要掌握好“修饰词”&#xff0c;这些修饰…

二叉树和堆

树概念及结构&#xff08;了解&#xff09; 树的概念&#xff08;看看就行&#xff09; 树是一种 非线性 的数据结构&#xff0c;它是由 n &#xff08; n>0 &#xff09;个有限结点组成一个具有层次关系的集合。 把它叫做树是因 为它看起来像一棵倒挂的树&#xff0c;也就是…

C语言 指针_野指针 指针运算

野指针&#xff1a; 概念&#xff1a;野指针就是指针指向的位置是不可知的&#xff08;随机的、不正确的、没有明确限制的&#xff09; 指针非法访问&#xff1a; int main() {int* p;//p没有初始化&#xff0c;就意味着没有明确的指向//一个局部变量不初始化&#xff0c;放…

腾讯 Hunyuan3D-2: 高分辨率3D 资产生成

腾讯 Hunyuan3D-2&#xff1a;高分辨率 3D 资产生成的突破 前言 在当今数字化时代&#xff0c;3D 资产生成技术正变得越来越重要。无论是游戏开发、影视制作还是虚拟现实领域&#xff0c;高质量的 3D 模型和纹理都是创造沉浸式体验的关键。然而&#xff0c;传统的 3D 资产制作…

Java如何实现反转义

Java如何实现反转义 前提 最近做的一个需求&#xff0c;是热搜词增加换一批的功能。功能做完自测后&#xff0c;交给了测试伙伴&#xff0c;但是测试第二天后就提了一个bug&#xff0c;出现了未知词 levis。第一眼看着像公司售卖的一个品牌-李维斯。然后再扒前人写的代码&…

Java 高级工程师面试高频题:JVM+Redis+ 并发 + 算法 + 框架

前言 在过 2 个月即将进入 3 月了&#xff0c;然而面对今年的大环境而言&#xff0c;跳槽成功的难度比往年高了很多&#xff0c;很明显的感受就是&#xff1a;对于今年的 java 开发朋友跳槽面试&#xff0c;无论一面还是二面&#xff0c;都开始考验一个 Java 程序员的技术功底…

后端:MyBatis

文章目录 1. MyBatis1-1. Mybatis 工具类的封装1-2. Mybatis 通过集合或实体类传递参数-实现插入数据(增)1-3. MyBatis 实现删除数据(删)1-4. MyBatis 实现修改数据(改)1-5. MyBatis 实现查询数据(查) 2. MyBatis 配置文件中的一些标签和属性2-1.environments标签2-2. dataSour…

安卓14自由窗口圆角处理之绘制圆角轮廓线

背景&#xff1a; 前面文章已经分享过&#xff1a; 如何一行代码搞定自由窗口的圆角处理&#xff1f;-wms/自由窗口/sf实战开发 但是又有学员朋友提出另一个blog的成果&#xff1a; 安卓aosp14上自由窗口划线边框Freeform Caption实战开发-千里马framework实战 想要把划线和…

【Unity3D】3D物体摆放、场景优化案例Demo

目录 PlaceManager.cs(放置管理类) Ground.cs(地板类) 和 GroundData.cs(地板数据类) 额外知识点说明 1、MeshFilter和MeshRenderer的Bounds区别 2、Gizmos 绘制一个平行于斜面的立方体 通过网盘分享的文件&#xff1a;PlaceGameDemo2.unitypackage 链接: https://pan.baid…

高效沟通驱动LabVIEW项目成功

在LabVIEW项目开发中&#xff0c;由于涉及软件、硬件及多方协作&#xff0c;项目沟通效率的高低直接影响开发进度与最终质量。不明确的需求、信息传递中的误解以及跨部门协作的阻碍&#xff0c;常导致项目延误甚至失败。因此&#xff0c;建立高效的沟通机制&#xff0c;确保信息…

信息收集(下)

一.端口信息收集 1.端口基础认知 A.端口简介 在 Internet 环境中&#xff0c;各主机依据 TCP/IP 协议实现数据包的发送与接收。数据包凭借目的主机的 IP 地址&#xff0c;在互联网络里完成路由选择&#xff0c;进而精准地传至目标主机。然而&#xff0c;当目的主机同时运行多…