线程同步之Volatile

2023-10-28

编译器优化

c#编译器会在不改变我们的意图的情况下做一些优化,比如:

a = 1;
a = 2;

编译器编译之后,可能就只剩下第二行了。
再比如:

a = 1;
b = a;

编译器优化后,可能会把第二行优化成b = 1
再比如:

a = m;
b = n;

编译器生成IL时,有可能会改变两行代码的顺序。
以上变化都是在编译器认为不改变作者意图的前提下做的,如果在单线程环境下这也没有问题,但是如果是多线程环境操作一个公共资源的话,先读后读或先写后写,都可能会造成不确定的结果。

运行时优化

在运行时,为了提高运行速度,CPU读取一个变量时,可能会从内存中把它加载到CPU的寄存器内,下次再读取这个变量的话直接从寄存器中读取。但在多线程环境下,如果这是一个公共变量的话,第二次读取之前这个变量可能会在别的线程中被修改了而它却不知道。

使用Volatile实现线程同步

System.Threading.Volatile类提供了两个静态方法

public static bool Read (ref bool location);
public static void Write (ref int location, int value);
Volatile.Write方法保证两点:
  1. 将值写入到变量所对应的内存地址中。由ref关键字可以看得出来。
  2. 如果Volatile.Write方法之前有读写location的操作,那么编译器生成的IL代码也保证这些代码必须出现在Volatile.Write之前。Volatile.Write之后的代码则不保证。
Volatile.Read也方法保证了两点
  1. 总是从location的内存地址中读取值。(而不会从CPU的寄存器内读取).
  2. 如果Volatile.Read方法之后有读写location的操作,那么编译器生成的IL代码也保证这些代码必须出现在Volatile.Read之后。Volatile.Read之前的代码则不保证。

Volatile.Write和Volatile.Read的第1点结合起来,就保证了对一个公共变量的读取总是可以得到它最新的值。第2点结合起来的意思就是,总是最后调用Volatile.Write写入最后一个值,并且总是最先调用Volatile.Read读取最新的值,这就保证了我们的代码能够不被编译器搞乱了。

使用Volatile的例子

下面我们用一个例子来展示一下Volatile的强大之处。下面的例子同时运行着两个线程,一个线程只打印单数,另一个线程只打印双数,通过Volatile的静态方法来读写一个公共资源来控制两个线程交叉打印。

        class Program
        {
            static bool isSingle = true;
            static void Main(string[] args)
            {
                Parallel.Invoke(() => PrintList1(), () => PrintList2());
            }

            static void PrintList1()
            {
                for (int i = 0; i < 100; i = i + 2)
                {
                    while (true)
                    {
                        if (Volatile.Read(ref isSingle) == true)
                        {
                            Console.WriteLine(i);
                            Volatile.Write(ref isSingle, false);
                            break;
                        }
                        Thread.Yield();
                    }
                }
            }

            static void PrintList2()
            {
                for (int i = 1; i < 100; i = i + 2)
                {
                    while (true)
                    {
                        if (Volatile.Read(ref isSingle) == false)
                        {
                            Console.WriteLine(i);
                            Volatile.Write(ref isSingle, true);
                            break;
                        }
                        Thread.Yield();
                    }
                }
            }
        }

上面代码的运行结果是以非常快的速度从0打印到99。

我解释一下Thread.Yield()方法的含义:如果windows发现有另一个线程已经准备好在当前CPU上执行,则会结束调用该方法的线程的剩余CPU时间而被选中的线程会得到一个CPU时间片,这种情况下返回true,然后,调用Yield的线程会再次被调度,开始一个全新的CPU时间片。如果windows发现没有已经准备好在当前CPU执行的线程,则调用Yield的线程会继续运行它的剩余时间片,这种情况下返回false。

由于这个方法比较依赖windows api,所以在dotnet core的版本中还没有提供,如果你使用的是dotnet core,则可以使用Thread.Sleep(1)来达到我们本例中的目的。Thread.Sleep()方法的含义是:调用该方法的线程自动放弃自己当前剩余的CPU时间并且在指定的时间内不再被CPU调度。但.net并不保证指定时间过去后该线程会立刻被调度。

volatile关键字

c#还提供了volatile关键字来简化对Volatile的静态方法的调用,但我觉得这个关键字虽然简化了使用,但是却会迷惑我们,会掩盖真相。所以,我更喜欢直接使用Volatile.Write()和Volatile.Read()这两个静态方法。

对Volatile的误解

我看过一些人写的博客,包括一些老外写的博客,很多人都对Volatile有一些不太正确的理解。

有人认为编译器会保证Volatile上下文中代码的顺序,其实不对,Volatile只保证Write()方法之前的代码一定出现在Write()之前,Read()方法之后的代码一定出现在Read()之后

有些人认为Volatile很神奇,一个线程使用Volatile更新一个共有变量后,这个共有变量的变化会通知给所有读取这个变量的其它线程,其实也不对,真相是,Write()保证把值更新到内存中,Read()保证从内存地址读取值,而不从CPU寄存器或其它缓存中读取。

看下面的代码

    int value = 0;
    void ChangeValue(int addValue)
    {
        int temp = Volatile.Read(ref value);
        Volatile.Write(ref value, temp + addValue);
    }

有人认为Volatile.Read总能对读取到最新的值,所以上面的ChangeValue()方法在多线程环境下并发执行也不会有问题,其实也不对,因为有可能两个线程同时读取到原来的值,并且同时更新这个值,就造成了最终的值是错误的,因为有一次更新丢失了。

那么,如果解决上面的问题呢?其实在上面的代码中,读取是一个原子级的操作,写入是一个原子级的操作,但读取和写入不是一个原子级的操作,这就造成了并发时的更新丢失。正确的方法是使用Interlocked.Add(ref value,addValue),这个方法实现了原子级的读写。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

线程同步之Volatile 的相关文章

随机推荐

  • JAVA高级知识点整理

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 前言 String字符串类 String 可变字符串 StringBuilder 可变字符串与String之间的转换 RunTime类 概述 特点 使用 Data类
  • apache beam入门之程序入口——PipelineOption

    前置章节apache beam入门宝典之初次使用 从第一章里我们看到最开始需要生成1个PipelineOption 然后才能生成1个Pipeline 而这个所谓的option用处是将可以将程序输入的args数组转成1个PipelineOpt
  • 【基于python实现UI自动化】3.0 selenium - webdriver常见8大元素定位

    python UI自动化之selenium元素定位 1 0 selenium工具介绍 2 0 selenium环境搭建 3 0 selenium元素定位 selenium常见8大元素定位 通过ID定位 通过class name定位 通过na
  • [开发过程]<c#上位机>关于.net6

    水下机器人 c 上位机 根据官方文档进行学习开发 1 了解 net6 简而言之 就是稳定强悍 跨设备 简单上手 资源丰富 强 Announcing NET 6 The Fastest NET Yet NET Blog NET 6 is no
  • java类的参考文献,太完整了!

    一面 先是问了问项目 然后就开始问一些问题 1 每个请求耗时100ms 机器的配置是4核8G 问要达到10000TPS需要多少台机器 没答上来 问了问是IO密集型还是CPU密集型 然后面试官说我想得太复杂了 2 怎么实现网页的自动跳转 答3
  • 将json文件解析存储到MySQL数据库

    PostMapping test public Object test RequestParam file MultipartFile file 将JSON解析为Java对象也称为从JSON反序列化Java对象 ObjectMapper o
  • Spring Cloud 2.x之整合工作流Activiti

    工作流在项目中非常常用 这里先来看两张图 第一张 第二张 对以上两张图进行说明 假设这两张图就是华谊兄弟的请假流程图 图的组成部分 人物 范冰冰 冯小刚 王中军 事件 动作 请假 批准 不批准 工作流 Workflow 就是 业务过程的部分
  • LLVM 环境搭建

    LLVM相关 环境搭建 PC VMware Workstation 下载 https www vmware com go getworkstation win KEY ZC3WK AFXEK 488JP A7MQX XL8YF 可自行网上查
  • Vue中使用qrcode实现渲染生成二维码中间添加自定义logo-demo

    效果 安装 npm i qrcode 使用 import QRCode from qrcode 具体生成过程
  • Mac OS X下Maven的安装与配置

    Mac OS X 安装Maven 下载 Maven 并解压到某个目录 例如 Users robbie apache maven 3 3 3 打开Terminal 输入以下命令 设置Maven classpath vi bash profil
  • 小程序打开速度慢是服务器原因吗,网页打开速度慢的原因以及解决方法

    现在大多数企业都会选择做自己的官方网站 网站的作用更像一张互联网上的企业名片 客户能否选择你 在于网站的质量 网站质量的好与坏 主要取决于网站的流量 而影响网站流量最核心的因素就是网站打开速度 那么今天就来给大家分享一下 网页打开速度慢的原
  • CTFHUB-布尔盲注

    布尔盲注 页面回显的结果只有两种 true 和 false true false 常用函数 count x 返回统计的数量 length str 返回str字符串的长度 ascii str 返回字符串str的最左面字符的ASCII代码值 s
  • vue3-实战-06-管理后台-品牌管理模块开发

    目录 1 品牌列表 1 1 需求图 1 2 定义接口和数据类型 1 3 请求接口和渲染数据 2 新增和修改品牌 2 1 需求原型分析 2 2 dialog开发 2 3 请求接口封装 2 4 图片上传组件开发 2 5 新增 修改品牌信息 3
  • pb中计算两个时间的分钟_利用Power BI中的两个函数,灵活计算各种占比!

    计算个体占总体的比例是一个很常见的分析方式 它很简单 就是两个数字相除 但是当需要计算的维度 总体的范围发生动态变化时 如何灵活且快速的计算出各种占比 还是需要动一点心思的 本文就通过 DAX 中的 ALL 和 ALLSELECTED 函数
  • Qt中的信号和信号槽(一)

    目录 1 信号和槽概述 信号和槽的关系 2 标准信号槽使用 标准信号 槽 示例 3 自定义信号槽使用 自定义信号 自定义槽 示例 1 信号和槽概述 信号和槽是一种事件驱动的通信机制 广泛应用于Qt框架的事件处理 GUI编程 网络通信等方面
  • pytorch计算模型参数量报错:size mismatch for module.conv1.weight: copying a param with shape torch.Size([16, 3

    错误 RuntimeError Error s in loading state dict for DataParallel size mismatch for module conv1 weight copying a param wit
  • FIO使用说明(最详细最全的参数说明)

    这个文档是对fio 2 0 9 HOWTO文档的翻译 fio的参数太多了 翻译这个文档时并没有测试每一个参数的功能和使用方法 只有少量参数做了试验 大部分的参数采用的是根据字面翻译或是个人理解的翻译 必然有些出入 先发出来 以后有使用的时候
  • 迁移学习花式Finetune方法大汇总

    如果觉得我的算法分享对你有帮助 欢迎关注我的微信公众号 圆圆的算法笔记 更多算法笔记和世间万物的学习记录 迁移学习广泛地应用于NLP CV等各种领域 通过在源域数据上学习知识 再迁移到下游其他目标任务上 提升目标任务上的效果 其中 Pret
  • JS之arguments、arguments.callee、caller介绍

    arguments 调用函数时产生的 保存实参 arguments callee 被调用时指向函数自身 caller 指向调用某函数的那个函数 下面通过一段代码说明它们的用处 function A n console log argumen
  • 线程同步之Volatile

    编译器优化 c 编译器会在不改变我们的意图的情况下做一些优化 比如 a 1 a 2 编译器编译之后 可能就只剩下第二行了 再比如 a 1 b a 编译器优化后 可能会把第二行优化成b 1 再比如 a m b n 编译器生成IL时 有可能会改