.Net 多线程、异步、性能优化应用与心得

文章目录

    • 概要
    • 多线程
      • Thread方式创建线程:
      • Task方式创建线程[C#5.0引入](推荐使用):
      • 线程池方式创建线程:
    • 异步
      • 异步方法
      • 异步IO操作
      • 异步数据库操作
      • 异步Web请求
      • 取消异步
      • ValueTask[C# 7.0引入]
        • ValueTask<TResult> 和 Task
    • 性能优化
      • 懒加载对象
      • 循环
        • List<T>.ForEach
        • Foreach(推荐)
        • for
    • 小结

概要

本文主要介绍在.net 8中如何进行大数据处理会用到多线程,异步,以及性能优化,读写锁,缓存等相关知识。

多线程

在处理大量数据的时候往往对处理速度是有需求的,越快越好。如果想要快的话的我们必定会想到多线程,然而在.net Core中线程的创建模式多种多样,我们改选择哪一种呢?每一种创建方式的各有什么优缺点呢?

Thread方式创建线程:

 static void Main()
    {
		      List<Thread> threads = new List<Thread>();
		List<string> val = new List<string>() { "参数1", "参数2", "参数3", "参数4", "参数5", "参数6" };
		foreach (var item in val)
		{
		    var thread= new Thread(() => { Console.WriteLine("这是线程:"+ item); });
		    threads.Add(thread);
		    thread.Start();
		}
		foreach (var thread in threads)
		{
		    thread.Join();
		}
    }

执行效果:
在这里插入图片描述
这种方式能保证线程执行顺序,因为我们后面是用的join(),这种方式会创建六个线程用完之后就会被销毁掉。
如果数据量少和下面说的Task创建方式体现不出来太明显的差距,但是如果数据量大的话,频繁的创建和销毁线程肯定是行不通的。

Task方式创建线程[C#5.0引入](推荐使用):

static async Task Main(string[] args)
    {
		  List<Task> tasks = new List<Task>();
		 List<string> val = new List<string>() { "参数1", "参数2", "参数3", "参数4", "参数5", "参数6" };
		 foreach (var item in val)
		 {
		     var task= Task.Factory.StartNew(() => { Console.WriteLine("这是:"+ item); });
		     tasks.Add(task);
		 }
		 await Task.WhenAll(tasks);
    }
 

执行效果:
在这里插入图片描述
这种方式可以看出来这六个线程明显没有顺序可言。相对于上面说的Thread方式这种方式的执行其实是并行的。
而且这种方式的底层实现其实是才用的线程池,当线程使用完之后不会立即销毁,会放回线程池内,等到下次再处理的时候就可以直接使用这个线程,这样就避免了频繁的创建和销毁线程,减少了性能开销。当然它也不是一直存放在线程池里就不销毁了,如果真是这样那迟早得爆炸,线程池会自动维护它,当它闲置了较长时间之后也是会被自动销毁掉的。

线程池方式创建线程:

 static void Main()
    {
		   		 int ThreadNumber = 6;
		     var doneEvents = new ManualResetEvent[ThreadNumber];
		List<string> val = new List<string>() { "参数1", "参数2", "参数3", "参数4", "参数5", "参数6" };
		   foreach (var item in val)
		{
		    int _index = val.IndexOf(item);
		    doneEvents[_index] = new ManualResetEvent(false);
		    ThreadPool.QueueUserWorkItem((state) =>
		    {
		        int _index = val.IndexOf(item);
		        Console.WriteLine(val[_index]);
		        doneEvents[_index].Set();
		    }, item);
		}
		WaitHandle.WaitAll(doneEvents);
    }

执行效果:
在这里插入图片描述
这种方式也是没有顺序的,其实和task效果是一样的,因为task底层就是才用的线程池实现的,但是这种方式相对于Task来说更为麻烦。

异步

聊完了多线程咱们再来聊一聊在.net Core中很常见的异步,异步的使用想必大家都会吧,在此之前我想提醒大家一下
如果方法的处理速度很快,或者你的代码执行后立即可用等,使用异步并不会比同步快,反而有可能多消耗性能资源

异步方法

public async Task<int> GetNumberAsync()
{
    await Task.Delay(1000); //模拟长时间运行的任务
    return 42;
}

这里 async 和await 底层逻辑是通过状态机实现的分段执行。当一个方法被标记为async时,编译器会生成一个状态机类,该类实现了IAsyncStateMachine接口。状态机负责管理异步操作的执行流程,包括启动、暂停、继续和完成等状态‌,方法上加了async编译器就会生成两个方法,一个同步方法一个异步方法,程序执行的时候会调用同步方法。
同步方法中再调用生成的那个异步方法,异步方法则会创建了一个状态机,将参数传给状态机,并调用Start方法,可知异步方法实际上是状态机方法的调用。
不太理解的话可以看一看这位博主的文章写得很详细的 传送门

异步IO操作

Stopwatch stopwatch = Stopwatch.StartNew();//记录时间
    string FilePath = "E:\\测试项目\\Test";
    string FileName = "测试文本写入.txt";
   
    for (int i = 0; i < 10; i++)
    {
        await Task.Run(async () =>
        {
            await Console.Out.WriteLineAsync("当前线程ID:"+ Thread.CurrentThread.ManagedThreadId);
            var Write = await ReadWriteFileAsync(FilePath, FileName, $"测试写入{i}");

            await Console.Out.WriteLineAsync(Write.Item2);

            var Read = await ReadFileAsync(FilePath + "\\" + FileName);

            await Console.Out.WriteLineAsync(Read.Item2);

            Console.ForegroundColor = ConsoleColor.Green;
            await Console.Out.WriteLineAsync(Read.Item2);
            Console.ResetColor();
          
        });
    }
    stopwatch.Stop();
    Console.WriteLine($"任务执行耗时: {stopwatch.ElapsedMilliseconds} 毫秒");


    static async Task<(bool,string)> ReadFileAsync(string filePath)
    {
        try
        {
            string content = await File.ReadAllTextAsync(filePath);
           return(true,"读取成功:"+content);
        }
        catch (Exception e)
        {
            return  (false,$"读取文件失败:{e.Message}");
        }
    }
    
     static async Task<(bool,string)> ReadWriteFileAsync(string filePath,string fileName,string text)
    {
       
        if(string.IsNullOrWhiteSpace(filePath)&&string.IsNullOrWhiteSpace(fileName))
        {
            return (false,"路径或文件名称不能为空!");
        }
        if(!fileName.Contains("."))
        {
            return (false, "文件名称需要加上文件类型后缀!");
        }
        try
        {
            string path =$"{filePath}\\{fileName}";
            if (File.Exists(path))
            {
                //文件已存在
                await File.AppendAllTextAsync(path, text);
                return (true, "追加成功!");
            }
            else
            {//文件未存在
                await File.WriteAllTextAsync(path, text);
                return (true, "写入成功!");
            }
        }
        catch (Exception ex)
        {
            return (false, "写入错误:" + ex.Message);
        }
       
        
    } 

Console.ReadLine();

执行效果: 在这里插入图片描述
这里我是记录的执行耗时的,总共花费了97毫秒,其实多数耗时都是花费在了创建线程上面,采用多线程的方式去进行文件的写入和读取,可以看出异步操作不会阻塞调用线程,适合在高并发场景下提高程序的整体性能,如果每次写入都是一个用户发起的请求的话那么也可以说它可以更有效地利用系统资源,比如在网络应用中,可以处理多个网络连接而不会阻塞。

异步数据库操作

常见的ORM几乎都提供的异步操作的方法。像SqlSugar、EF Core、Dapper 都提供的异步查询相关的异步方法。
各自的官方也有相关的介绍,这里就不过多赘述了,具体实现细节都各有千秋,有兴趣的同学可以研究他们的源代码。
在异步操作时需要注意并发问题,比如在高并发时同时去修改同一条数据,一定要用主键作为条件,这样可以减少事务死锁发生的概率。用主键的话数据库表使用的是行锁,如果时用到主键外的其他条件进行判断则会锁表

异步Web请求

.net Core 中有三种发起Http请求的方式

  • HttpWebRequest
    它在System.Net命名空间下。它派生自WebRequest, 这个类非常强大,强大的地方在于它封装了几乎HTTP请求报文里需要用到的东西,以致于能够能够发送任意的HTTP请求并获得服务器响应(Response)信息。采集信息常用到这个类。但是对于新手小白来说它的配置又太复杂了。而且想要使用现代化异步编程的话实现起来也不太方便。

  • HttpClient
    这个方法要慎用,用不好就会TCP 连接和疯狗一样向上猛蹿。可以看看这位博主写的文章:传送门

  • IHttpClientFactory(推荐

IHttpClientFactory 是在 .NET Core 2.1 版本引入的,用于创建和管理 HttpClient 实例的新型机制。它提供了中心化的配置,管理 Logging 和 Distributed caching 的能力,以及客户端的注册和复用 可以方便地管理HTTP客户端的生命周期,例如,通过依赖注入容器来管理,可以自动处理依赖关系和连接池。支持外部配置,如负载均衡,长时间运行的HTTP连接,以及服务发现。

三种方式的使用方式:传送门

取消异步

在这里插入图片描述

很多异步方法参数列表中都会有 CancellationToken 这个参数,那这个参数的作用是什么呢?想必大家已经猜到了它就是用来取消异步的关键。
那么帅气的彦祖们又会提问了,好好的异步取消它干啥?
比如说异步下载文件的时候异步请求超时了、异步查询数据库时连接超时等情况就需要结束异步了,避免一直耗费性能。
它是一个轻量级对象,可以通知请求是否已取消,我们可以手动调用 它里面的Cancel()方法来取消任务

示例代码(摘抄自: 天才卧龙):

static async Task Main(string[] args)
        {
            CancellationTokenSource source = new CancellationTokenSource();
            source.CancelAfter(4 * 1000);//运行时间超过4秒,则取消执行
          
            try
            {
                await DownLoadPage_3("http://www.baidu.com", 200, source.Token);
                //输入q 请求被取消
                while (Console.ReadLine() == "q")
                {
                    source.Cancel();
                }
            }
            catch
            { 
                Console.WriteLine($"下载超时被取消了");
            }
        }
        //简单的下载任务
        public static async Task DownLoadPage_3(string uri, int num, CancellationToken token)
        {
            using (HttpClient client = new HttpClient())
            {
                for (int i = 0; i < num; i++)
                {
                    var html = await client.GetAsync(uri, token);
                    Console.WriteLine($"第{i + 1}次下载");
                    //抛出被取消的异常
                    token.ThrowIfCancellationRequested();
                }
            }
        }

ValueTask[C# 7.0引入]

ValueTask 和 Task

ValueTask 存在于 System.Threading.Tasks 命名空间下,ValueTask 的定义如下:
在这里插入图片描述

IEquatable<T> 接口定义 Equals 方法,用于确定两个实例是否相等。
Task 的定义如下:

public class Task : IAsyncResult, IDisposable

微软官方文档的大概意思就是ValueTask这个类型,应该是 Task 的简化版本,Task 是引用类型,因此从异步方法返回 Task 对象或者每次调用异步方法时,都会在托管堆中分配该对象。
这里我们就可以总结出
这就是它俩主要的不同之处

	Task 是引用类型,会在托管堆中分配内存;ValueTask 是值类型;

ValueTask 使用方法:

 static async ValueTask<int> StartAsync(int val)
{
    Task<int> task = Task.Run<int>(() => val + 1);
    return await new ValueTask<int>(task);
}
int val= await  StartAsync(3);
Console.WriteLine("返回值"+val);
Console.ReadLine();

执行效果:
在这里插入图片描述
如果想更深入了解的话可以去看看这位大佬写的文章:溪源More

性能优化

懒加载对象

想要懒加载对象可以用Lazy类来实现。Lazy可以确保在实际访问对象之前不会创建它
如下代码中new Lazy的时候是不会创建对象的直到访问.Value属性才会创建相关对象。

示例代码:

Lazy<CW> lazyObject = new Lazy<CW>();

// 在实际访问对象之前,不会创建它
if (lazyObject.IsValueCreated)
{
    Console.WriteLine("对象已经被创建");
}
else
{
    Console.WriteLine("对象尚未被创建");
}
// 下面的代码将触发对象的创建
CW actualObject = lazyObject.Value;
Console.WriteLine("程序结束");
Console.ReadLine();

public class CW
{
    public CW()
    {
        // 这里可以是耗时的初始化代码
        Console.WriteLine("对象 被创建");
    }
}

执行效果
在这里插入图片描述

循环

List.ForEach

List.ForEach是List类中的一个方法,允许你对列表中的每个元素执行一个指定的懂做,通过传递一个Action委托,它极大的简化了代码的编写,比如假设我们有一个List想打野出每个元素可以这样写

List<string> strs = new List<string>() { "字符串1", "字符串2", "字符串3", "字符串4" };
strs.ForEach(t => Console.WriteLine(t));

执行效果
在这里插入图片描述

Foreach(推荐)

C# 中的关键字,能够遍历任何实现了IEnumerable接口的集合

List<string> strs = new List<string>() { "字符串1", "字符串2", "字符串3", "字符串4" };
foreach (string str in strs)
{
    Console.WriteLine(str);
}

执行效果:
在这里插入图片描述

for

for循环是一种控制结构,用于重复执行一组语句,直到指定的条件返回false‌

List<string> strs = new List<string>() { "字符串1", "字符串2", "字符串3", "字符串4" };
for (int i = 0; i < strs.Count; i++)
{
    Console.WriteLine(strs[i]);
}

三种方式的性能对比

for循环和foreach对决:

  • 功能上的区别:foreach用于遍历集合,而for除了遍历集合,还可以用于执行一系列固定次数的迭代操作。
  • 性能上的区别:在遍历集合时,如果你不需要知道元素的索引,通常建议使用foreach,因为这样更简洁,更易于阅读。在需要访问索引时,for循环更为合适。但在性能上,两者几乎没有差异。
  • 用法上的区别:foreach语法更简洁,不需要在循环体中显式地处理索引的增减。而for循环需要显式地控制循环变量的初始化、循环条件和迭代操作。

foreach和List.ForEach对决:

  • 性能上的区别:List.ForEach在遍历的时候会创建额外的委托实力,因此在大量数据处理的时候使用foreach效率会更高一些
  • 灵活性的区别:foreach 可以在循环中使用 break 或 continue 来控制循环的进行。相对而言,List.ForEach 则缺乏这种直接控制的能力,只能在回调函数中执行具体逻辑。
  • 可读性的区别:List.ForEach 更像是函数式编程的风格,特别适合处理集合中的操作。而 foreach 提供了更直观、可读的语法。

小结

长路漫漫,学习编程就是一个不断学习成长的过程,就像游戏一样打怪升级。一定要保持住那份探索的热情。加油我的朋友!

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

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

相关文章

如何在项目中使用人大金仓替换mysql

文章目录 数据库连接配置调整驱动和连接字符串修改&#xff1a;用户名和密码&#xff1a; SQL 语法兼容性检查数据类型差异处理&#xff1a;函数差异&#xff1a;SQL语句客户端 SQL 交互工具 数据迁移数据库、用户移植数据迁移工具使用&#xff1a;迁移过程中的问题及解决方案 …

Docker 安装 sentinel

Docker 安装系列 1、拉取 [rootTseng ~]# docker pull bladex/sentinel-dashboard Using default tag: latest latest: Pulling from bladex/sentinel-dashboard 4abcf2066143: Pull complete 1ec1e81da383: Pull complete 56bccb36a894: Pull complete 7cc80011dc6f: Pull…

十一、容器化 vs 虚拟化-Docker 使用

文章目录 前言一、Docker Hello World二、Docker 容器使用三、Docker 镜像使用四、Docker 容器连接五、Docker 仓库管理六、Docker Dockerfile七、Docker Compose八、Docker Machine九、Swarm 集群管理 前言 Docker 使用‌ Docker 容器使用、镜像使用、容器连接、仓库管理、Do…

【报错】新建springboot项目时缺少resource

1.问题描述 在新建springboot项目时缺少resources,刚刚新建时的目录刚好就是去掉涂鸦的resources后的目录 2.解决方法 步骤如下&#xff1a;【文件】--【项目结构】--【模块】--【源】--在main文件夹右击选择新建文件夹并命名为resources--在test文件夹右击选择新建文件夹并命名…

Java面试之Happens-Before原则

此篇接上一篇的Java面试之volatile关键字。 首先&#xff0c;这是Java语言中的一个“先行发生”(Happens-Before)的原则。是判断数据是否存在竞争&#xff0c;线程是否安全的非常有用的手段&#xff0c;也是Java内存模型中定义的两项操作之间的偏序关系。 其次&#xff0c;Happ…

AB plc设备数据 转 opc ua项目案例

目录 1 案例说明 2 VFBOX网关工作原理 3 准备工作 4 网关采集AB PLC数据 5 启动OPC UA协议转发采集的数据 6 案例总结 1 案例说明 设置网关采集AB PLC数据把采集的数据转成opc ua协议转发给其他系统。 2 VFBOX网关工作原理 VFBOX网关是协议转换网关&#xff0c;是把一种…

ASP.NET|日常开发中连接Sqlite数据库详解

ASP.NET&#xff5c;日常开发中连接Sqlite数据库详解 前言一、安装和引用相关库1.1 安装 SQLite 驱动1.2 引用命名空间 二、配置连接字符串2.1 连接字符串的基本格式 三、建立数据库连接3.1 创建连接对象并打开连接 四、执行数据库操作4.1 创建表&#xff08;以简单的用户表为例…

Redis篇-6--原理篇5--单线程模型

1、概述 Redis 采用单线程模型来处理客户端请求&#xff0c;这意味着在任意时刻只有一个命令被执行。这种设计简化了 Redis 的实现&#xff0c;并确保了高并发环境下的数据一致性。尽管 Redis 是单线程的&#xff0c;但它通过高效的内存管理和网络 I/O 操作&#xff0c;仍然能…

Spring Boot + Spring AI快速体验

Spring AI快速体验 1 什么是Spring AI主要功能 2 快速开始2.1 版本说明2.2 配置文件2.3 pom依赖2.3.1 spring maven仓库2.3.2 核心依赖 2.4 定义ChatClient2.5 启动类2.6 测试 3 参考链接 1 什么是Spring AI Spring AI是Spring的一个子项目&#xff0c;是Spring专门面向于AI的…

【Unity】【VR开发】摩托车游戏开发笔记1-摩托车手把旋转时轴位移问题

【背景】 做VR摩托车游戏时,需要给摩托车加仿真控制,其中就有抓握龙头旋转时转弯的实现。 实现分两部分,一个是视觉上的动画实现,一个是摩托车实际的位移控制实现。先实现动画效果,也就是抓握把手能够让车头左右旋转。这里先简单一点,实现左手单手让车头旋转。 【设计】…

uniapp -- 实现页面滚动触底加载数据

效果 首选,是在pages.json配置开启下拉刷新 {"path": "pages/my/document/officialDocument","style": {"navigationStyle":</

丹摩|丹摩助力selenium实现大麦网抢票

丹摩&#xff5c;丹摩助力selenium实现大麦网抢票 声明&#xff1a;非广告&#xff0c;为用户体验 1.引言 在人工智能飞速发展的今天&#xff0c;丹摩智算平台&#xff08;DAMODEL&#xff09;以其卓越的AI算力服务脱颖而出&#xff0c;为开发者提供了一个简化AI开发流程的强…

Android系统(android app和系统架构)

文章目录 AndroidAndroid Apps四大组件 Android系统Platform API之下&#xff1a;一个微笑内核adb(Android Debug Bridge) Android包管理机制Android的Intent机制参考 Android LinuxFrameworkJVM 在Linux/Java上做了个二次开发&#xff1f;并不完全是&#xff1a;Android定义…

小程序开发中的插件生态与应用-上

更多精彩内容都在公zhong号&#xff1a;小白的大数据之旅 在小程序的开发过程中&#xff0c;插件作为扩展功能、提升效率的重要工具&#xff0c;扮演着不可或缺的角色。它们不仅能够帮助开发者快速集成复杂的功能模块&#xff0c;还能优化开发流程&#xff0c;缩短项目周期。 …

An error happened while trying to locate the file on the Hub and we cannot f

An error happened while trying to locate the file on the Hub and we cannot find the requested files in the local cache. Please check your connection and try again or make sure your Internet connection is on. 关于上述comfy ui使用control net预处理器的报错问…

Java 实现给pdf文件指定位置盖章功能

Java 实现给pdf文件指定位置盖章功能 开发中遇到一个需求, 需要给用户上传的的pdf文件, 指定位置上盖公章的功能, 经过调研和对比, 最终确定实现思路. 这里是使用pdf文件中的关键字进行章子的定位, 之所以这样考虑是因为如果直接写死坐标的话, 可能会出现因pdf大小, 缩放, 盖章…

Vmware的网络适配器的NAT模式和桥接模式有何区别?如何给Uubunt系统添加桥接网卡?

Vmware的网络适配器的NAT模式和桥接模式有何区别&#xff1f; 如何给Uubunt系统添加桥接网卡? 步骤如下&#xff1a;

主机连不上CentOS7虚拟机Redis

CentOS7中的Redis连不上主机 是否ping通 先尝试主机是否能Ping通虚拟机 虚拟机中查看ens33对应的地址&#xff0c;使用ifconfig 再在主机上尝试Ping&#xff0c;如果无法Ping通&#xff0c;先排除是否是虚拟机NAT或者桥接模式配置的问题 redis.conf配置 我是按照黑马的教…

vue element 切换 select 下拉框的 单选多选报错

今天根据项目需求&#xff0c;需要对下拉框进行&#xff0c;单双选判断&#xff0c;当多选切换成多选&#xff0c;没有问题但是单选切换成多选报错如下 页面是要求 选择in或者notin时候 多选 经过好长时间摸索&#xff0c;解决了&#xff0c;最后使用select的失去焦点事件解决的…

VBA高级应用30例应用在Excel中的ListObject对象:向表中添加注释

《VBA高级应用30例》&#xff08;版权10178985&#xff09;&#xff0c;是我推出的第十套教程&#xff0c;教程是专门针对高级学员在学习VBA过程中提高路途上的案例展开&#xff0c;这套教程案例与理论结合&#xff0c;紧贴“实战”&#xff0c;并做“战术总结”&#xff0c;以…