C#Lazy 实现延迟加载详解与示例

在C#中,Lazy< T> 类是一个非常有用的工具,它可以用于延迟加载值,尤其是在创建对象时可能很昂贵,或者你想要延迟初始化直到真正需要该值的情况下。在本文中,我们将详细介绍 Lazy< T> 的实现机制和用法,并提供一些示例来展示它的优势。

1、Lazy 的工作原理

Lazy< T> 类是.NET框架中的一个并发类,它允许你延迟初始化一个对象,直到这个对象被第一次使用时才进行。这意味着,如果多个线程需要访问同一个延迟初始化的对象,Lazy< T> 能够保证只有一个线程会执行初始化代码,从而避免不必要的资源消耗。

Lazy< T> 采用懒汉式初始化模式,在.NET Framework 4.0及之前的版本中,它是线程安全的,采用内部互斥锁(Mutex)来确保线程安全。但在.NET 4.0之后,Lazy< T> 采用了新的LazyInitializationMode.None模式,允许非线程安全且更高效的初始化,这时需要开发者自己确保初始化的线程安全。

2、创建 Lazy 实例

要创建一个 Lazy< T> 实例,你可以使用以下构造函数:

Lazy<T>() : this(LazyThreadSafetyMode.ExecutionAndPublication)
Lazy<T>(Func<T> valueFactory) : this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication)
Lazy<T>(LazyThreadSafetyMode mode)
Lazy<T>(Func<T> valueFactory, LazyThreadSafetyMode mode)

LazyThreadSafetyMode 是一个枚举,用于指定初始化时的线程安全模式。有四种模式:

  • LazyThreadSafetyMode.None:允许非线程安全初始化。
  • LazyThreadSafetyMode.ExecutionAndPublication:执行初始化时是线程安全的,且Publish方法也是线程安全的。
  • LazyThreadSafetyMode.PublicationOnly:仅Publish方法是线程安全的。
  • LazyThreadSafetyMode.UnprotectedPublication:既不是执行时也不是发布时线程安全。

3、 使用 Lazy

一旦你创建了一个 Lazy< T> 实例,你可以通过其 Value 属性来获取其内部值的引用,该属性是只读的,并会在第一次访问时触发值的初始化。

4、示例

下面我们通过一个示例来演示如何使用 Lazy 进行延迟加载。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        // 使用 Lazy<T> 创建一个延迟加载的对象
        Lazy<ExpensiveObject> lazyExpensiveObject = new Lazy<ExpensiveObject>(() => new ExpensiveObject(), LazyThreadSafetyMode.ExecutionAndPublication);

        // 获取对象值,这将触发延迟加载
        ExpensiveObject expensiveObject = lazyExpensiveObject.Value;

        // 使用 expensiveObject 做些事情
        Console.WriteLine(expensiveObject.SomeProperty);

        Console.ReadKey();
    }
}

class ExpensiveObject
{
    public ExpensiveObject()
    {
        // 模拟一个初始化代价很昂贵的操作
        Console.WriteLine("Expensive object initialized.");
    }

    public string SomeProperty { get; set; }
}

在这个示例中,我们创建了一个 ExpensiveObject 的 Lazy 实例。这个 ExpensiveObject 的构造函数是一个耗时的操作。当我们第一次访问 lazyExpensiveObject.Value 时,构造函数会被调用,并且 ExpensiveObject 实例会被创建。注意,之后对这个属性的所有访问都会直接返回已经创建的实例,而不会再次调用构造函数。

注意事项

  1. 如果你需要在多个线程中共享延迟加载的对象,请确保你正确同步对这个对象的访问。
  2. 如果你的初始化操作是线程安全的,你可以使用 LazyThreadSafetyMode.ExecutionAndPublication,这样可以保证初始化过程和发布过程都是线程安全的。
  3. 如果你的初始化操作不依赖于外部状态,并且你确信它可以在多个线程中安全地并行执行,你可以使用 LazyThreadSafetyMode.None,这将避免线程锁定,并可能提高性能。

5、Lazy< T> 实现延迟加载

Lazy< T> 利用了 C# 的属性器和反射机制来实现延迟加载。当访问 Lazy< T> 的 Value 属性时,如果内部值尚未初始化,则初始化它。这个过程称为“lazy initialization”。Lazy< T> 提供了几种不同的线程安全模式,以适应不同的场景。

实现方式

下面是使用 Lazy 进行延迟加载资源的基本步骤:

  1. 创建一个 Lazy 实例,并通过提供一个函数来指定要延迟加载的资源。
  2. 在需要的时候,通过访问 Lazy 的 Value 属性来触发资源的加载。

示例:延迟加载图片

假设我们有以下一个类,它使用 Lazy 来延迟加载图片:

using System;
using System.Drawing;
using System.Threading.Tasks;

public class ImageLoader
{
    private Lazy<Bitmap> _lazyImage = new Lazy<Bitmap>(() => LoadImageAsync("path/to/image.jpg"), LazyThreadSafetyMode.ExecutionAndPublication);

    public Bitmap GetImage()
    {
        return _lazyImage.Value;
    }

    private async Task<Bitmap> LoadImageAsync(string imagePath)
    {
        using (var stream = new FileStream(imagePath, FileMode.Open))
        {
            return (Bitmap)Image.FromStream(stream);
        }
    }
}

在这个例子中,ImageLoader 类有一个 Lazy 实例,它通过异步方法 LoadImageAsync 加载图片。当调用 GetImage 方法时,Lazy 会触发 LoadImageAsync 的执行,并返回图片。

示例:延迟加载视频

视频加载通常涉及到更复杂的操作,下面是一个简化的例子:

using System;
using System.IO;
using System.Threading.Tasks;

public class VideoLoader
{
    private Lazy<FileStream> _lazyVideoStream = new Lazy<FileStream>(() => LoadVideoAsync("path/to/video.mp4"), LazyThreadSafetyMode.ExecutionAndPublication);

    public FileStream GetVideoStream()
    {
        return _lazyVideoStream.Value;
    }

    private async Task<FileStream> LoadVideoAsync(string videoPath)
    {
        return new FileStream(videoPath, FileMode.Open);
    }
}

在这个例子中,VideoLoader 类使用 Lazy 来延迟加载视频文件流。当 GetVideoStream 被调用时,视频文件流会被创建并返回。

示例:延迟加载音频

音频文件的加载可以类似于视频文件的加载:

using System;
using System.IO;
using System.Threading.Tasks;

public class AudioLoader
{
    private Lazy<FileStream> _lazyAudioStream = new Lazy<FileStream>(() => LoadAudioAsync("path/to/audio.wav"), LazyThreadSafetyMode.ExecutionAndPublication);

    public FileStream GetAudioStream()
    {
        return _lazyAudioStream.Value;
    }

    private async Task<FileStream> LoadAudioAsync(string audioPath)
    {
        return new FileStream(audioPath, FileMode.Open);
    }
}

在这个例子中,AudioLoader 类使用 Lazy 来延迟加载音频文件流。当 GetAudioStream 被调用时,音频文件流会被创建并返回。

6、如何在多线程环境中测试 Lazy<T> 的线程安全性?

在多线程环境中测试 Lazy< T> 的线程安全性通常涉及到模拟 concurrent access(并发访问)来确保 Lazy< T> 在不同线程之间正确地处理初始化和访问。这里有几种方法可以用来测试 Lazy< T> 的线程安全性:

  1. 使用 Lazy< T> 的同步模式: 在 Lazy 的构造函数中指定 LazyThreadSafetyMode.ExecutionAndPublication 或 LazyThreadSafetyMode.PublicationOnly,这样 Lazy< T> 会确保在多个线程中的执行和发布都是线程安全的。
  2. 手动同步: 如果你使用的是 LazyThreadSafetyMode.None,你需要手动同步对 Lazy< T> 属性的访问。这可以通过 lock 语句或 Monitor 类来实现。
  3. 使用 Task 和 Parallel 类: 使用 Task 并行库来创建多个任务,每个任务访问 Lazy 的 Value 属性。确保在所有任务都完成时,Lazy< T> 的初始化只执行一次。
  4. 使用 Mutex 或 Semaphore: 使用 Mutex 或 Semaphore 来控制对 Lazy< T> 初始化代码的访问,确保初始化是独占进行的。
  5. 单元测试: 编写单元测试来模拟并发访问。可以使用测试框架(如 NUnit 或 xUnit)来创建多个测试线程,并确保它们正确地访问 Lazy< T>。
  6. 代码分析工具: 使用像 NDepend 或 SonarQube 这样的代码分析工具来检测可能的线程安全问题。
    下面是一个简单的示例,展示了如何在单元测试中使用 Lazy< T> 和 Task 来测试线程安全性:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

public class LazyTestClass
{
    private Lazy<List<int>> _lazyList = new Lazy<List<int>>(() => new List<int>(), LazyThreadSafetyMode.ExecutionAndPublication);

    public List<int> GetList()
    {
        return _lazyList.Value;
    }
}

public class Program
{
    public static void Main()
    {
        LazyTestClass testClass = new LazyTestClass();

        // 创建多个任务来并发访问 Lazy<T>
        var tasks = Enumerable.Range(1, 10).Select(i => Task.Run(() => testClass.GetList()));

        // 等待所有任务完成
        Task.WaitAll(tasks.ToArray());
    }
}

// 单元测试
public class LazyTestClassTests
{
    [Fact]
    public void TestLazyThreadSafety()
    {
        LazyTestClass testClass = new LazyTestClass();

        // 创建多个测试线程
        var tasks = Enumerable.Range(1, 10).Select(i => Task.Run(() => testClass.GetList()));

        // 等待所有任务完成
        Task.WaitAll(tasks.ToArray());

        // 断言列表的实例只有一个
        Assert.Single(tasks.Select(t => t.Result));
    }
}

在这个示例中,我们创建了一个 LazyTestClass,它有一个 Lazy<List> 成员。我们在主函数中创建了多个 Task 来并发地访问 GetList 方法,该方法返回 Lazy<List> 的值。在单元测试中,我们使用 Fact 属性来标记一个测试方法,并使用 Assert.Single 来断言只有一个 List 实例被创建。

7、Lazy 加载在性能和用户体验方面的作用

Lazy 加载技术可以显著提高程序的性能和用户体验。以下是它在不同方面的一些潜在作用:

  • 性能提升:通过延迟加载昂贵的资源,程序可以在不需要这些资源时避免不必要的开销。这意味着资源只有在真正需要时才会被加载,从而减少内存和CPU的使用。
  • 响应性增强:在用户界面(UI)中使用 Lazy 加载可以避免在初始加载时延迟UI的响应。这对于创建快速启动的应用程序至关重要。
  • 资源优化:对于大型资源,如图片、视频和音频文件,Lazy 加载确保只有在用户请求时才加载它们,这样可以减少应用程序的整体大小和加载时间。
  • 多线程支持:Lazy 加载在多线程环境中自动同步,这意味着不必担心在多个线程中共享和初始化资源的问题。

8、安全性和效率考虑

尽管 Lazy 加载提供了许多好处,但在使用时也需要考虑安全和效率:

  • 线程安全:Lazy 加载默认是线程安全的,但在自定义 Lazy 实现或使用 LazyThreadSafetyMode.None 时,需要确保线程安全。
  • 资源泄漏:如果异步加载的资源没有正确管理(例如,没有释放或关闭流),可能会导致资源泄漏。
  • 性能开销:即使是 Lazy 加载,如果初始化过程很昂贵,或者在短时间内多次调用 Value 属性,这可能会导致性能问题。
  • 过度依赖:过度使用 Lazy 加载可能会导致代码难以理解和维护,特别是当依赖关系变得复杂时。

总结

Lazy< T> 是C#中一个非常有用的并发特性,它允许开发者延迟初始化对象,直到这些对象真正被需要。通过正确使用 Lazy< T>,你可以优化应用程序的性能,减少资源消耗,并提高应用程序的响应性。

在使用 Lazy< T> 时,你需要仔细考虑线程安全问题,并选择合适的 LazyThreadSafetyMode。此外,你还需要确保在多个线程中共享延迟加载的对象时,你的初始化代码和发布代码都是线程安全的。

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

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

相关文章

[激光原理与应用-90]:光功率计基本原理

目录 一、光功率计原理 二、光功率计硬件电路 三、光功率计探头 四、接口信号 一、光功率计原理 光功率计是用来测量光功率的仪器&#xff0c;其原理基于光电效应和电信号的检测与处理。 下面是光功率计的基本原理&#xff1a; 光电效应&#xff1a; 光功率计使用光敏元件…

链表的分割

题目 现给定一链表的头指针 phead 以及值 x&#xff0c;需编写一段代码将所有小于 x 节点的排在其余节点之前&#xff0c;且不能改变原来的数据顺序&#xff0c;最后返回重新排列后的链表的头指针。 算法思想 将小于x的尾插在第一个链表 将大于等于x的尾插在第二个链表 最后…

1142 - SELECT command denied to user ···

MySql子账户操作数据库权限不够&#xff0c;提示错误 1142 - SELECT command denied to user database 1142 - ALTER command denied to user database 以下命令可以解决 GRANT SELEC your_database_name TO mysql_account%;

centos7上搭建mongodb数据库

1.添加MongoDB的YUM仓库&#xff1a; 打开终端&#xff0c;执行以下命令来添加MongoDB的YUM仓库&#xff1a; sudo vi /etc/yum.repos.d/mongodb-org-4.4.repo 在打开的文件中&#xff0c;输入以下内容&#xff1a; [mongodb-org-4.4] nameMongoDB Repository baseurlh…

【Mysql】用frm和ibd文件恢复mysql表数据

问题 总是遇到mysql服务意外断开之后导致mysql服务无法正常运行的情况&#xff0c;使用Navicat工具查看能够看到里面的库和表&#xff0c;但是无法获取数据记录&#xff0c;提示数据表不存在。 这里记录一下用frm文件和ibd文件手动恢复数据表的过程。 思路 1、frm文件&…

第一个Spring Boot程序

目录 一、Spring Boot介绍 二、创建Spring Boot项目 1、插件安装&#xff08;专业版不需要&#xff09; 2、创建SpringBoot项目 &#xff08;1&#xff09;这里如果插件下载失败&#xff0c;解决方案&#xff1a; &#xff08;2&#xff09;项目启动失败&#xff0c;解决…

Docker镜像与容器操作

一、Docker 镜像操作 1.1 搜索镜像 格式&#xff1a;docker search 关键字 docker search nginx 1.2 获取镜像nginx 格式&#xff1a;docker pull 仓库名称[:标签] 如果下载镜像时不指定标签&#xff0c;则默认会下载仓库中最新版本的镜像&#xff0c;即选择标签为 latest…

SwiftUI 5.0(iOS 17.0)触摸反馈“震荡波”与触发器模式趣谈

概览 要想创作出一款精彩绝伦的 App&#xff0c;绚丽的界面和灵动的动画并不是唯一吸引用户的要素。有时我们还希望让用户真切的感受到操作引发的触觉反馈&#xff0c;直击使用者的灵魂。 所幸的是新版 SwiftUI 原生提供了实现触觉震动反馈的机制。在介绍它之后我们还将进一步…

HBase的简单学习三

一 过滤器 1.1相关概念 1.过滤器可以根据列族、列、版本等更多的条件来对数据进行过滤&#xff0c; 基于 HBase 本身提供的三维有序&#xff08;行键&#xff0c;列&#xff0c;版本有序&#xff09;&#xff0c;这些过滤器可以高效地完成查询过滤的任务&#xff0c;带有过滤…

Redis中的缓存击穿、缓存穿透、缓存雪崩问题

1.什么是缓存击穿&#xff1f; 客户端恶意访问一个不存在的数据&#xff0c;从而造成穿透缓存&#xff0c;请求直接到达数据库&#xff0c;频繁的发送这一类的请求&#xff0c;直接查询数据库&#xff0c;数据库的压力变大。 1.1如何解决缓存击穿呢&#xff1f; 1&#xff0…

基于harris角点和RANSAC算法的图像拼接matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 MATLAB2022a 3.部分核心程序 ....................................................................... I1_harris fu…

【MySQL]】数据库操作指南之数据库的基础操作

&#x1f331;博客主页&#xff1a;青竹雾色间 &#x1f331;系列专栏&#xff1a;MySQL探险日记 &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞⭐收藏➕关注 ✨人生如寄&#xff0c;多忧何为 ✨ 文章目录 1. 创建数据库2.数据库的编码集与校验集2.1 编码集 (Character…

嵌入式Python基础1-2

嵌入式Python基础1-2 条件语句 if elif else 随机数random eval while循环 for循环 水仙花数 循环else list 列表常用方法 增删改查 加排序 append remove pop index() 升序sort(&#xff09;降序 sort(reverseTrue) 反转 reverse&#xff08;&#xff09;…

ESP32开发

1、简介 1.1 种类 WIFI模块在PC上做为客户端、服务器&#xff0c;在STM32上做服务器的通讯。在物联网应用开发有重要作用&#xff0c;种类居多&#xff0c;如下图 红色方框的esp8266-01s型号的无限wifi模块就是本章学习的主要对象。 1.2 特点 小巧的尺寸&#xff1a;ESP-01…

SpanBert学习

SpanBERT: Improving Pre-training by Representing and Predicting Spans 核心点 提出了更好的 Span Mask 方案&#xff0c;也再次展示了随机遮盖连续一段字要比随机遮盖掉分散字好&#xff1b;通过加入 Span Boundary Objective (SBO) 训练目标&#xff0c;增强了 BERT 的性…

python自动生成SQL语句自动化

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 Python自动生成SQL语句自动化 在数据处理和管理中&#xff0c;SQL&#xff08;Structured …

WAF防范原理

目录 一、什么是WAF 二、纵深安全防御 WAF的组网模式 WAF配置全景 WAF端 服务器 攻击端 拦截SQL注入&#xff0c;XSS攻击&#xff0c;木马文件上传 要求&#xff1a; 使用WAF&#xff0c;通过配置策略要求能防御常见的web漏洞攻击&#xff08;要求至少能够防御SQL、XSS、文…

毕业设计注意事项

1.开题 根据学院发的开题报告模板完成&#xff0c;其中大纲部分可参考资料 2.毕设 根据资料中的毕设评价标准&#xff0c;对照工作量 3.论文 3.1 格式问题 非常重要&#xff0c;认真对比资料中我发的模板&#xff0c;格式有问题&#xff0c;答辩输一半&#xff01; 以word…

wireshark RTP分析参数

主要看丢弃和Delta&#xff0c; 丢弃就是丢掉的udp包&#xff0c;所占的比率 Delta是当前udp包接收到的时间减去上一个udp包接收到的时间 根据载荷可以知道正确的delta应该是多少&#xff0c;比如G711A&#xff0c;ptime20&#xff0c;那么delta理论上应该趋近于20. 这里的de…

C++面向对象程序设计 - 运算符重载

函数重载就是对一个已有的函数赋予新的含义&#xff0c;使之实现新的功能。因此一个函数名就可以用来代表不同功能的函数&#xff0c;也就是一名多用。运算符也可以重载&#xff0c;即运算符重载&#xff08;operator overloading&#xff09;。 一、运算符重载的方法 运算符重…