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

news2024/11/25 16:48:53

        在前面的章节中讨论了 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.coloradmin.cn/o/765835.html

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

相关文章

手机怎么编辑pdf?这几款工具可以实现

手机怎么编辑pdf&#xff1f;在如今的数字时代&#xff0c;PDF文档已经成为了一种标准的文件格式。然而&#xff0c;当我们需要编辑这些PDF文档时&#xff0c;我们往往需要使用电脑上的专业软件&#xff0c;这给我们带来了很大的不便。不过&#xff0c;有许多手机应用程序可以让…

AIGC 大模型纷纷部署,企业如何为 AI 数据降本增效

编辑 | 宋慧 出品 | CSDN 云计算 AIGC 从年初开始持续爆火&#xff0c;国内各种大模型纷纷涌现&#xff0c;其中模型参数轻松突破千亿数量级。模型中数据的形态、部署也是多种多样的&#xff0c;庞大数据量背后的管理和成本不容小觑。 混合数据厂商肯睿 Cloudera 今年相继发布…

设计模式的概述

目录 分类 创建型模式 结构型模式 行为型模式 类之间的关系 关联关系 聚合关系 组合关系 依赖关系 继承关系 实现关系 设计原则 开闭原则 里氏代换原则 依赖倒转原则 接口隔离原则 合成复用原则 一、分类 创建型模式 用于描述“怎样创建对象”&#xff0c;它…

线程池的学习(一)

转载&#xff1a;Java 线程池 线程池的创建方式 方式一&#xff1a;创建单一线程的线程池 newSingleThreadExecutor 特点&#xff1a; 线程池中只包含 1 个线程&#xff0c;存活时间是无限的按照提交顺序执行任务唯一线程繁忙时&#xff0c;新提交的任务会被加入到阻塞队列…

Java springBoot项目报LDAP health check failed

报错内容如下&#xff1a; 在bootstrap.yml文件里加 management:health:ldap:enabled: false 配置。 或者在application.properties文件里加&#xff1a; management.health.ldap.enabledfalse 参考答案&#xff1a;LDAP health check failed 难道没有人遇到这样的问题吗&…

我造了一个新的词汇:信息湍流

信息湍流 信息湍流的简介起因有出现信息湍流的领域如何做信息湍流的计算 信息湍流的简介 在物流学中&#xff0c;一个物体从一个位置到另外一个位置&#xff0c;我们可以通过精确的公式计算来预测出新位置。 而水和气体则是大量一个一个物体组成的新物体&#xff0c;称为&…

Docker安装以及基础镜像的使用

Docker 安装 Docker 要求 CentOS 系统的内核版本高于 3.10 uname -r使用 root 权限登录 su # 输入密码更新 yum yum -y update卸载旧版本的 docker yum remove ‐y docker*安装需要的软件包 yum -y install yum-utils设置 yum 源&#xff0c;并更新 yum 包的索引 yum-config-ma…

凝心聚力,同为科技(TOWE)2023年中会议暨团建活动圆满举行

01凝心聚力 奋勇冲刺——年中会议现场 年中会议现场 时光冉冉&#xff0c;2023年已过半&#xff0c;7月4日-11日&#xff0c;为回望过往、总结成果、复盘经验&#xff0c;确保顺利完成公司年度目标&#xff0c;增强团队凝聚力、激发员工活力&#xff0c;深化企业文化建设&…

15 - 堆栈 - 大顶堆

前面我们学习了小顶堆,相信大家都已经有点概念了,今天来了解一下大顶堆。 大顶堆示意图 堆数组存放的公式 我们用简单的公式来描述一下堆的定义就是: 大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] 小顶堆:arr[i] <= arr[2i+1] && a…

两股热潮如何汇聚:当数字孪生遇上元宇宙

引言 随着科技的迅猛发展&#xff0c;数字孪生和元宇宙已成为当今技术界备受关注的两股热潮。这两个概念各自都在不同领域取得了突破性进展&#xff0c;但在最近的发展中&#xff0c;人们开始发现它们之间存在着潜在的契合点。本文将探讨数字孪生和元宇宙的定义、特点&#xf…

【【51单片机蜂鸣器实现起风了】】

哀伤如同风&#xff0c;消失无影踪。 前面的有两个非常关键的点希望兄弟们明白 我一开始也失算了 这里兄弟们注意务必改成int 不然会超出 就会出现播放一半从头开始的情况 下面是两份起风了代码直接附上main.c 因为另外的其实和我之前说的模板都一样复制粘贴就行 为什么是…

M1 Mac如何安装CentOS7虚拟机(图文详细解说)

1、下载相应的文件 2、打开VMware Fusion pro进行安装 3、 输入许可证密钥 4、 将CentOS-7拖入“从光盘或映像中安装”中 5、点击继续 6、选择其他-->其他64位ARM-->继续 7、进行自定设置 8、这里更改名为“Centos7”&#xff08;不要加空格&#xff09;&#xff0c;存…

2023 Testing Expo倒计时-聚焦Softing 9003展位

请点击此处&#xff0c;即可进行在线登记报名并了解更多信息&#xff01;

ES系列--文档处理

一、文档冲突 当我们使用 index API 更新文档 &#xff0c;可以一次性读取原始文档&#xff0c;做我们的修改&#xff0c;然后重 新索引 整个文档 。 最近的索引请求将获胜&#xff1a;无论最后哪一个文档被索引&#xff0c;都将被唯一存 储在 Elasticsearch 中。如果其他人同时…

Java-生成数据库设计文档

目录 场景screw 官网介绍接口编写 场景 在企业开发中&#xff0c;有些公司会要求开发人员编写数据库表结构文档&#xff0c;这项工作没啥技术含量而且很繁琐&#xff0c;每当有表发生更改时就需要维护这个文档&#xff0c;或者是需要交付数据库设计文档和导出数据库设计文档这类…

8月|龙讯旷腾高性能计算与工业材料模拟论坛2023

2023年8月25日 山东青岛 高性能计算与工业材料模拟论坛2023 青岛&#xff0c;别称岛城&#xff0c;国务院批复确定的中国沿海重要中心城市和滨海度假旅游城市&#xff0c;国家历史文化名城、中国帆船之都、世界啤酒之城、联合国电影之都&#xff0c;也是国家海洋科研和教育中…

【产品经理】TO B市场分析

市场分析是一个独立而又宏大的学科领域&#xff0c;并且具体使用中&#xff0c;目标和个体不同&#xff0c;分析的方式方法也不同。TO B产品的市场分析是对市场环境、市场规模、性质、特征、竞品进行分析&#xff0c;从而寻找和研究潜在需求的市场机会&#xff0c;帮助产品经理…

设计模式大白话——工厂模式

文章目录 设计模式大白话——工厂模式1.1、简单工厂:1.2、工厂方法1.3、抽象工厂 设计模式大白话——工厂模式 1.1、简单工厂: 场景与思路 ​ 现在需要开一个 Pizza 店&#xff0c;Pizza 店可以生产各种口味的 Pizza ​ 既然要生产各种各样的 Pizza&#xff0c;那就会很容易想…

管理类联考——英语——趣味篇——不择手段——d开头单词

&#x1f3e0;个人主页&#xff1a;fo安方的博客✨ &#x1f482;个人简历&#xff1a;大家好&#xff0c;我是fo安方&#xff0c;考取过HCIE Cloud Computing、CCIE Security、CISP、RHCE、CCNP RS、PEST 3等证书。&#x1f433; &#x1f495;兴趣爱好&#xff1a;b站天天刷&…

SignalTap II 软件使用步骤

文章目录 前言一、SignalTap II是什么&#xff1f;二、使用步骤三、总结四、参考资料 前言 环境&#xff1a; 1、Quartus18.1 2、板子型号&#xff1a;原子哥开拓者2(EP4CE10F17C8) 要求&#xff1a; 能够使用SignalTap II进行片上调试。 一、SignalTap II是什么&#xff1f; S…