【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.mfbz.cn/a/40612.html

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

相关文章

C#安装.Net平台科学计算库Math.Net Numerics

工作的时候需要使用到C#的Math.Net库来进行计算。 Math.Net库涵盖的主题包括特殊函数&#xff0c;线性代数&#xff0c;概率模型&#xff0c;随机数&#xff0c;插值&#xff0c;积分&#xff0c;回归&#xff0c;优化问题等。 这里记录一下&#xff0c;安装Math.Net库的过程…

el-date-picker组件的picker-options常规属性设置

查询已发生的配置项 // 日期选择器快捷键配置&#xff08;一般过去时&#xff09; pickerOptions: {shortcuts: [{text: 今天,onClick(picker) {let start new Date();let end new Date();picker.$emit(pick, [start, end]);}},{text: 昨天,onClick(picker) {let start new…

uniapp微信小程序使用axios(vue3+axios+ts版)

版本号 "vue": "^3.2.45", "axios": "^1.4.0", "axios-miniprogram-adapter": "^0.3.5", 安装axios及axios适配器&#xff0c;适配小程序 yarn add axios axios-miniprogram-adapter 使用axios 在utils创建utils/…

Flask SQLAlchemy_Serializer ORM模型序列化

在前后端分离项目中&#xff0c;经常需要把ORM模型转化为字典&#xff0c;再将字典转化为JSON格式的字符串。在遇到sqlalchemy_serializer之前&#xff0c;我都是通过类似Java中的反射原理&#xff0c;获取当前ORM模型的所有字段&#xff0c;然后写一个to_dict方法来将字段以及…

计算机vcruntime140.dll丢失的解决方法,重新安装教程

vcruntime140.dll是Microsoft Visual C Redistributable文件中的一个动态链接库&#xff08;DLL&#xff09;。这个文件是由Microsoft开发的&#xff0c;用于支持C编程语言的运行环境。vcruntime140.dll是Windows系统非常重要的文件&#xff0c;通常会被一些应用程序或游戏所需…

Jenkins | 获取凭证密码

目录 方法一&#xff1a;查看所有账号及密码 方法二&#xff1a;查看指定账号密码 方法一&#xff1a;查看所有账号及密码 Jenkins > 系统管理 > 脚本命令行 com.cloudbees.plugins.credentials.SystemCredentialsProvider.getInstance().getCredentials().forEach{i…

Linux进程

Linux进程 对于进程的理解&#xff0c;我们要从计算机的重要的冯诺依曼体系结构讲起&#xff0c;只有知道我们的程序/文件是如何在计算机中被操作运行并输出到显示器中&#xff0c;通过对于操作系统的理解&#xff0c;才能对于进程进行一定的理解。 文章目录 Linux进程冯诺依…

【Linux】udp服务器实现大型网络聊天室

udp终结篇~ 文章目录 前言一、udp服务器实现大型网络聊天室总结 前言 根据上一篇文章中对于英汉互译和远程操作的两个小功能大家应该已经学会了&#xff0c;我们的目的是让大家可以深刻的理解udp服务器所需要的接口已经实现的简单步骤&#xff0c;下面我们开始实现网络聊天室。…

3.SpringBoot 返回Html界面

1.添加依赖spring-boot-starter-web <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>2.创建Html界面 在Resources/static 文件夹下面建立对应的html&#xff0c…

快速排序的非递归实现、归并排序的递归和非递归实现、基数排序、排序算法的时间复杂度

文章目录 快速排序的非递归三数取中法选取key快速排序三路划分 归并排序的递归归并排序的非递归计数排序稳定性排序算法的时间复杂度 快速排序的非递归 我们使用一个栈来模拟函数的递归过程&#xff0c;这里就是在利用栈分区间。把一个区间分为 [left,keyi-1][key][keyi1,right…

用百度地图api获取当前定位,获取经纬度——前端笔记

问题&#xff1a; 做一个按钮&#xff0c;点击后可以获取到当前位置的经纬度&#xff0c;并渲染地图。 效果如下: 代码如下: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head><title>获取当前定位测试<…

【笔记MD】

https://editor.csdn.net/md/?not_checkout1&articleId131798584 这里写自定义目录标题 https://editor.csdn.net/md/?not_checkout1&articleId131798584欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入…

行业数据和报告到底应该如何去找?

信息时代&#xff0c;经常要对行业信息进行分析。这时首先就是要进行信息收集和筛选&#xff0c;如果我们懂得构建自己的工作工具和数据来源&#xff0c;效率会蹭蹭往上涨。 找行业报告、了解行业趋势&#xff0c;提高效率。 1. 国家权威 国家统计局&#xff1a;这个网站覆盖…

谋合作、创新境 | 百度参观图为科技生产全链路

当代科技的发展不断催生出新的变革和机遇&#xff0c;百度作为全球顶尖的高科技公司&#xff0c;凭借其强大的创新基因&#xff0c;一直处于人工智能领域的最前沿。   近日&#xff0c;百度公司派出了一支专业团队来到了图为科技&#xff0c;对图为的研发技术及生产线进行了全…

基于MSP432P401R跟随小车【2022年电赛C题】

文章目录 一、赛前准备1. 硬件清单2. 工程环境 二、赛题思考三、软件设计1. 路程、时间、速度计算2. 距离测量3. 双机通信4. 红外循迹 四、技术交流 一、赛前准备 1. 硬件清单 主控板&#xff1a; MSP432P401R测距模块&#xff1a; GY56数据显示&#xff1a; OLED电机&#x…

基于单片机智能台灯坐姿矫正器视力保护器的设计与实现

功能介绍 以51单片机作为主控系统&#xff1b;LCD1602液晶显示当前当前光线强度、台灯灯光强度、当前时间、坐姿距离等&#xff1b;按键设置当前时间&#xff0c;闹钟、提醒时间、坐姿最小距离&#xff1b;通过超声波检测坐姿&#xff0c;当坐姿不正容易对眼睛和身体腰部等造成…

Git 使用笔记

Git使用笔记 1 版本控制 1.1 什么是版本控制 ​ 版本控制&#xff08;Revision control&#xff09;是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史&#xff0c;方便查看更改历史记录&#xff0c;备份以便恢复以前的版本的软件工程技术。简单说就是用…

大模型开发(六):OpenAI Completions模型详解并实现多轮对话机器人

全文共8500余字&#xff0c;预计阅读时间约17~30分钟 | 满满干货(附代码)&#xff0c;建议收藏&#xff01; 代码下载点这里 一、 Completions与Chat Completions基本概念 经过海量文本数据训练的大模型会在全量语义空间内学习语法关系和表达风格&#xff0c;并通过某些微调过…

postgresql部署及优化

目录 一、postgresql概念 二、PostgreSQL 特征 三、postgres安装与登录 3.1、postgres安装 3.2、postgres使用 3.3.1、postgres登录 3.3.2、修改postgres用户密码 四、PostgreSQL 命令 4.1、PostgreSQL 创建数据库 4.2、删除数据库 4.3、创建表格 4.4、删除表格 一、…

Python爬虫学习笔记(一)————网页基础

目录 1.网页的组成 2.HTML &#xff08;1&#xff09;标签 &#xff08;2&#xff09;比较重要且常用的标签&#xff1a; ①列表标签 ②超链接标签 &#xff08;a标签&#xff09; ③img标签&#xff1a;用于渲染&#xff0c;图片资源的标签 ④div标签和span标签 &…
最新文章