文章目录
- 概要
- 多线程
- 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 提供了更直观、可读的语法。
小结
长路漫漫,学习编程就是一个不断学习成长的过程,就像游戏一样打怪升级。一定要保持住那份探索的热情。加油我的朋友!