【C#】并行编程实战:使用延迟初始化提高性能

        在前面的章节中讨论了 C# 中线程安全并发集合,有助于提高代码性能、降低同步开销。本章将讨论更多有助于提高性能的概念,包括使用自定义实现的内置构造。

        毕竟,对于多线程编程来讲,最核心的需求就是为了性能。

延迟初始化 - .NET Framework | Microsoft Learn探索 .NET 中的迟缓初始化,性能提高意味着对象创建被延迟到首次使用该对象时。icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/framework/performance/lazy-initialization        本教程对应学习工程:魔术师Dix / HandsOnParallelProgramming · GitCode


1、延迟初始化概念简析

        延迟加载(Lazy Load),也叫懒加载,是应用程序编程中常用的设计模式,指对对象的创建推迟到实际使用时才执行。延迟加载模式最常用的用法之一是在缓存预留模式(Cache Aside Pattern)中:对于创建时有很大开销的对象时,可以使用缓存预留模式将对象缓存以备用。

        书上的概念感觉挺复杂,大家可能没整明白,其实完全可以当做单例模式来理解。一般情况下,单例的写法会如下所示:

    /// <summary>/// 单例示例/// </summary>public class MySingleton{//限制构造函数,以避免外部类创建private MySingleton() { }//静态缓存预留private static MySingleton m_Instance;//单例获取public static MySingleton Instance{get{if (m_Instance == null)m_Instance = new MySingleton();//懒加载return m_Instance;}}}

        这里我们看到,只有在 m_Instance 为空时调用了单例获取时,才会对单例进行创建。这种创建单例的模式,就叫做懒加载。

        但是显然,上述代码对线程支持并不好。因为如果多个线程来对单例进行获取,可能就会创建多次,也就是线程不安全。如果要线程安全,则需要加锁,并使用双重检查锁定,示例如下:

        private static object m_LockObj = new object();//单例获取public static MySingleton Instance{get{//第一次判定if (m_Instance == null){//锁定共享数据lock (m_LockObj){//第二次判定,因为可能在等待锁定的过程中,就已经实例化过了。if (m_Instance == null)m_Instance = new MySingleton();//懒加载}}return m_Instance;}}

        当然,我们这种单例只是延迟加载的一种特殊案例,延迟加载还有很多其他用处。但对于多线程而言,从头开始实现延迟加载通常都比较复杂,但 .NET Framework 为延迟模式提供了专门的类库。

2、关于 System.Lazy<T>

        .NET Framework 提供了一个 System.Lazy<T> 类,具有延迟初始化的所有优点,开发人员无需担心同步开销。当然,System.Lazy<T> 类的创建将被推迟到首次访问他们之前。

Lazy提供对延迟初始化的支持。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.lazy-1?view=netstandard-2.1        这里我们先写一个目标类的示例:

    /// <summary>/// 测试用类/// </summary>public class DataWrapper{public DataWrapper(){Debug.Log($"DataWrapper 被创建了!");}public void HandleX(int x){Debug.Log($"DataWrapper 执行了:{x}");}}

        这个类很简单,也就是创建的时候会打印一行 Log;然后里面有个实例的执行方法,会打印一个 int 值出来。接下来使用 Lazy<T> :

        private void RunWithLazySimple(){Lazy<DataWrapper> lazyDataWrapper = new Lazy<DataWrapper>();Debug.Log("开始 : RunWithLazySimple");Task.Run(async () =>{await Task.Delay(1000);Parallel.For(0, 5, x =>{lazyDataWrapper.Value.HandleX(x);});Debug.Log("执行完毕!");});}

        执行结果如下:

        可见 lazyDataWrapper 在第一次使用时才会被创建,这里是系统自动调用了无参的构造函数进行构建。这个和我们之前写的单例代码效果是一样的。

        当然,Lazy<T> 还会有其他的写法,比如使用工厂方法函数:

Lazy<DataWrapper> lazyDataWrapper = new Lazy<DataWrapper>(GetDataWrapper);public static DataWrapper GetDataWrapper()
{return new DataWrapper();
}

        这个方法(没有传入更多参数)默认就是线程安全的,当然也可以有别的地方可以设置。


        关于 LazyThreadSafetyMode

LazyThreadSafetyMode 枚举 (System.Threading) | Microsoft Learn指定 Lazy<T> 实例如何同步多个线程间的访问。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.lazythreadsafetymode?view=netstandard-2.1#--

  • None:不是线程安全
  • PublicationOnly:完全线程安全,多个线程都会初始化,但最终只保留一个实例,其余均放弃。

  • ExecutionAndPublication:完全线程安全,使用锁定来确保只有一个线程初始化该值。


3、使用延迟初始化模式处理异常

        延迟对象在设计上是不可变的(单例),也就是每次返回的都是同一个实例。但,如果自初始化时出错了,会发生什么情况?

        这里我们把上述实例代码改一下:

        public DataWrapper(int x){Debug.Log($"DataWrapper 被创建了!但是带参数:{x}");paramX = 1000 / x;}

        当我们传 0 的时候就会有除 0 错误。之后测试代码如下:

        private void RunWithLazyError(){Lazy<DataWrapper> lazyDataWrapper = new Lazy<DataWrapper>(TestFunction.GetDataWrapperError);Debug.Log("开始 : RunWithLazyFunc");Task.Run(async () =>{await Task.Delay(1000);Parallel.For(0, 5, x =>{try{lazyDataWrapper.Value.HandleX(x);}catch (Exception ex){Debug.LogError(ex.Message);}});Debug.Log("执行完毕!");});}

        意,TryCatch 代码一定要在 lazyDataWrapper 取值的地方框起来。在 Task 外面框起来并不会报错。甚至在构造函数里面框起来也不会报出来。运行一下:

         结果非常有意思啊,实际上只执行了一次初始化(然后出错了),但后续几次调用系统都直接返回了错误。如果将 LazyThreadSafetyMode 改为 PublicationOnly,则会出现 5 次初始化,并报 5 个错误。

        在第一次取值时,如果是 ExecutionAndPublication 模式下发生了异常,那么之后都会一直返回这个初始化失败的异常。而在 PublicationOnly 模式下,如果前一次取值错误,后一次仍然会尝试初始化,直到成功为止。

4、线程本地存储的延迟初始化

        在学习此章节内容前,先看一段代码:

private static int TestValue = 1;for (int i = 0; i < 10; i++)Task.Run(() => Debug.Log(TestValue));

        那么这段代码,打印出来会是什么结果?答案显而易见,就是 10 个 1,想都不用想。

4.1、ThreadStatic

        如果我们给 TestValue 加上属性 ThreadStatic 会如何?

        在 Unity 上,打印的结果将会是 10 次 0 (只有在主线程使用时,其值是 1)。

ThreadStaticAttribute 类 (System) | Microsoft Learn指示各线程的静态字段值是否唯一。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.threadstaticattribute?view=netstandard-2.1        被 ThreadStatic 标记的属性其初始值只会在构造函数时赋值一次,而对于其他线程,将仍保持 Null 或者默认值。

4.2、ThreadLocal<T>

        ThreadStatic 虽然能保证每个线程都能拿到一个独立的值,但是不能给他赋初始值,每次都是默认值还是有些不方便。如果确实需要赋值初始值,就可以使用 ThreadLocal<T>:

        public void RunWtihThreadLocal(){ThreadLocal<DataWrapper> lazyDataWrapper = new ThreadLocal<DataWrapper>(TestFunction.GetDataWrapper);Task.Run(() =>{Parallel.For(0, 5, x =>{lazyDataWrapper.Value.HandleX(1);lazyDataWrapper.Value.HandleX(2);lazyDataWrapper.Value.HandleX(3);lazyDataWrapper.Value.HandleX(4);});});}

        像上述代码,每个线程获取的时候都会初始化一次,但也只会初始化这一次:

         但是 ThreadLocal 和 Lazy 除了线程分配之外,还有以下区别:

  • ThreadLocal 的 Value 是读写的。

  • 没有任何初始化逻辑,ThreadLocal 将获得 T 的默认值(而 Lazy 会调用无参构造函数)。

5、减少延迟初始化的开销

        这一章其实就讲了一个类的使用方法:

LazyInitializer 类 (System.Threading) | Microsoft Learn提供延迟初始化例程。 icon-default.png?t=N6B9https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.lazyinitializer?view=netstandard-2.1        看起来和 Lazy 差不多,而且使用还更复杂了,要怎么理解呢?Lazy 其实是包装了一个基础对象来间接使用,可能会导致计算和内存问题,但 LazyInitializer 就能避免包装对象。我们先看一个例子:

        private DataWrapper m_DataWrapper;private bool m_IsInited;private object m_LockObj = new object();public void RunWithLazyInitializer(){Task.Run(() =>{Parallel.For(0, 5, x =>{var value = LazyInitializer.EnsureInitialized(ref m_DataWrapper, ref m_IsInited, ref m_LockObj, TestFunction.GetDataWrapper);value.HandleX(x);});});}

        运行结果如下:

         可见运行效果和 Lazy 一样的。但是由于使用的是原对象,我们可以对原对象进行格外操作。虽然我个人认为,大部分情况下 LazyInitializer 和 Lazy 差别并不大。


6、本章小结

        本章讨论了延迟加载的各个方面以及 .NET Framework 提供的使延迟架子啊更易于实现的数据结构。但值指出的是,延迟加载本身有设计上的缺陷:程序员并不能确认它究竟是何时初始化的,有时甚至会在不想突其初始化时初始化,或者本来就该卸载了,反而又初始化了,从而引发各种问题。

        就和单例一样,我个人任务初始化应该受控地放在一起,而不是使用延迟加载(懒加载)。这样可以在框架层面确保初始化和释放。

         本教程对应学习工程:魔术师Dix / HandsOnParallelProgramming · GitCode

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

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

相关文章

GoFrame v2.5 版本发布,企业级 Golang 开发框架

大家好啊&#xff0c;GoFrame 框架今天发布了 v2.5.0 正式版本啦&#xff01;&#x1f44f;&#x1f44f;&#x1f44f;&#x1f44f; 本次版本主要是对已有功能组件以及开发工具上的改进工作。其中&#xff0c;开发工具新增了 gf gen ctrl 命令&#xff0c;以规范化定义、开发…

Bash 第十行

195 第十行 给定一个文本文件 file.txt&#xff0c;请只打印这个文件中的第十行。 示例: 假设 file.txt 有如下内容&#xff1a; Line 1 Line 2 Line 3 Line 4 Line 5 Line 6 Line 7 Line 8 Line 9 Line 10 你的脚本应当显示第十行&#xff1a; Line 10 来源&#xff1a;…

Hive概述

Hive 一 Hive基本概念 1 Hive简介 学习目标 - 了解什么是Hive - 了解为什么使用Hive####1.1 什么是 Hive Hive 由 Facebook 实现并开源&#xff0c;是基于 Hadoop 的一个数据仓库工具&#xff0c;可以将结构化的数据映射为一张数据库表&#xff0c;并提供 HQL(Hive SQL)查询…

【uni-app】自定义导航栏

【uni-app】自定义导航栏 新手刚玩uniapp进行微信小程序&#xff0c;甚至多端的开发。原生uniapp的导航栏&#xff0c;并不能满足ui的需求&#xff0c;所以各种查阅资料&#xff0c;导航栏自定义内容 整理如下&#xff1a; 需要修改的文件如下&#xff1a; 1、pages.json 修…

SpringBoot读取配置的6种方式

1. 概述 通过了解springboot加载配置&#xff0c;可以更方便地封装自定义Starter。 在SpringBoot中&#xff0c;可以使用以下6种方式读取 yml、properties配置&#xff1a; 使用Value注解&#xff1a;读取springboot全局配置文件单个配置。使用Environment接口&#xff1a;通过…

流程工业停机的实际成本

流程制造工厂面临着避免停机的巨大压力&#xff0c;因为这可能会严重影响企业的整体生产力、盈利能力和声誉。企业对计划外停机的原因和成本了解得越多&#xff0c;就能做更多的事情来帮助降低停机的发生率&#xff0c;并在停机发生时更好地做好应对准备。 图.石油炼化工厂&…

【2023 年第二届钉钉杯大学生大数据挑战赛】 初赛 B:美国纽约公共自行车使用量预测分析 问题一Python代码分析

2023 年第二届钉钉杯大学生大数据挑战赛 初赛 B&#xff1a;美国纽约公共自行车使用量预测分析 问题一 1 题目 Citi Bike是纽约市在2013年启动的一项自行车共享出行计划&#xff0c;由“花旗银行”(Citi Bank)赞助并取名为“花旗单车”(Citi Bike)。在曼哈顿&#xff0c;布鲁克…

王道计算机网络学习笔记(4)——网络层

前言 文章中的内容来自B站王道考研计算机网络课程&#xff0c;想要完整学习的可以到B站官方看完整版。 四&#xff1a;网络层 ​​​​​​​​​​​​​​在计算机网络中&#xff0c;每一层传输的数据都有不同的名称。 物理层&#xff1a;传输的数据称为比特&#xff08;Bi…

vmware-ubuntu 出现的奇怪问题

虚拟机突然连不上网 参考博文-CSDN-卍一十二画卍&#xff08;作者&#xff09;-Vmware虚拟机突然连接不上网络【方案集合】 sudo vim /var/lib/NetworkManager/NetworkManager.statesudo service network-manager stop sudo vim /var/lib/NetworkManager/NetworkManager.stat…

git -- SSL certificate problem

SSL certificate problem 1.问题描述 新建一个仓库&#xff0c;在向里面上传文件时&#xff0c;出现SSL证书问题 2.解决方法 这个问题是由于没有配置信任的服务器HTTPS验证。默认&#xff0c;cURL被设为不信任任何CAs&#xff0c;就是说&#xff0c;它不信任任何服务器验证。…

HOT64-搜索二维矩阵

leetcode原题链接&#xff1a;搜索二维矩阵 题目描述 给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非递减顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff0c;如果 target 在矩阵中&#xff0c;返回…

【机器学习】吴恩达课程1-Introduction

一、机器学习 1. 定义 计算机程序从经验E中学习&#xff0c;解决某一任务T&#xff0c;进行某一性能P&#xff0c;通过P测定在T上的表现因经验E而提高。 2. 例子 跳棋程序 E&#xff1a;程序自身下的上万盘棋局 T&#xff1a;下跳棋 P&#xff1a;与新对手下跳棋时赢的概…

用Python采用Modbus-Tcp的方式读取PLC模块数据

使用计算器得到需要的寄存器地址 这里PLC地址是83,对应的程序16进制读取地址是53 实际上由于PLC地址从1开始&#xff0c;所以这里实际地址应该是52&#xff0c;因为计算机从0开始 使用网络调试助手生成报文 使用Python中的内置函数int()。以下是将人员卡号’b’3b44’转换为十…

解决appium-doctor报gst-launch-1.0.exe and/or gst-inspect-1.0.exe cannot be found

一、下载gst-launch-1.0.exe and gst-inspect-1.0.exe 下载地址&#xff1a;Download GStreamer runtime installer 和 development installer 两个应用程序都要下载并安装 二、运行安装 下载好后点击安装会弹出如下界面&#xff0c;点击“更多信息”展开&#xff0c;点击“仍然…

拓宽“主航道”的Serverless与EDA领域,亚马逊云科技不断创新开拓

在新潮如走马灯般变换的时尚界&#xff0c;每隔几年就会刮起一阵复古风。被誉为“时尚教父”的著名设计师安德烈莱昂塔利曾说&#xff1a;“时尚总是在寻找新的灵感和方向&#xff0c;而复古是其中一个重要的来源。” 无独有偶。日新月异的高科技领域也会出现公认的“过时”…

msys2安装与配置: 在windows上使用linux工具链g++和包管理工具pacman C++开发

文章目录 为什么用这个msys2下载、doc安装&#xff0c;很简单初次运行&#xff0c;做些配置更新软件安装与卸载方法安装必要的软件包设置win环境变量在windows terminal中使用在vscode中使用 为什么用这个msys2 方便windows上的C开发demo&#xff0c;不需要VS了方便C开发安装o…

跨越山海,爱在滇西|拓数派为滇西孩子点亮科学梦想

近日&#xff0c;拓数派在共青团浙江大学委员会、景东县教育体育局和景东团县委等单位指导下开展“爱在滇西”2023年公益助学活动&#xff0c;并携手浙大国际科创中心、浙大微纳电子学院、启真科技控股公司和北京德恒律所共同向景东浙大求是中学捐赠爱心助学金&#xff0c;用于…

【RISC-V】昉·星光 2单板计算机初始调试记录

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

JavaSwing+MySQL的飞机订票系统(内含oracle版本)

点击以下链接获取源码&#xff1a; https://download.csdn.net/download/qq_64505944/88055544 JDK1.8 MySQL5.7 功能&#xff1a;接收客户端发来的数据、处理客户端发来的数据、发送数据包到客户端&#xff1b;客户端&#xff1a;查询所有航班的信息、查看自己所定的票、订票…

mac批量提取文件夹的名称,怎么操作?

mac批量提取文件夹的名称&#xff0c;怎么操作&#xff1f;很多小伙伴想知道在mac电脑上可以一键快速批量的将大量文件夹的名提取出来&#xff0c;而不是采用一个一个名称提取的方法&#xff0c;这是一个有利于提高工作效率的办法&#xff0c;这一项技能在网上几乎找不到解决办…