C# 多线程学习之基础入门
作者:Alan.hsiang 发布时间:2022-11-08 05:38:13
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源或正在使用它们。本文以一些简单的小例子,简述如何将程序由同步方式,一步一步演变成异步多线程方式,仅供学习分享使用,如有不足之处,还请指正。
同步方式
业务场景:用户点击一个按钮,然后做一个耗时的业务。同步方式代码如下所示:
private void btnSync_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
Console.WriteLine("************btnSync_Click同步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 5; i++)
{
string name = string.Format("{0}_{1}", "btnSync_Click", i);
this.DoSomethingLong(name);
}
Console.WriteLine("************btnSync_Click同步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
watch.Stop();
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
/// <summary>
/// 模拟做一些长时间的工作
/// </summary>
/// <param name="name"></param>
private void DoSomethingLong(string name)
{
Console.WriteLine("************DoSomethingLong 开始 name= {0} 线程ID= {1} 时间 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
//CPU计算累加和
long rest = 0;
for (int i = 0; i < 1000000000; i++)
{
rest += i;
}
Console.WriteLine("************DoSomethingLong 结束 name= {0} 线程ID= {1} 时间 = {2} 结果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
}
同步方式输出结果,如下所示:
通过对以上示例进行分析,得出结论如下:
同步方式按顺序依次执行。
同步方式业务和UI采用采用同一线程,都是主线程。
同步方式如果执行操作比较耗时,前端UI会卡住,无法响应用户请求。
同步方式比较耗时【本示例9.32秒】
异步多线程方式
如何优化同步方式存在的问题呢?答案是由同步方式改为异步异步多线程方式。代码如下所示:
private void btnAsync_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
Console.WriteLine("************btnAsync_Click异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
Action<string> action = new Action<string>(DoSomethingLong);
for (int i = 0; i < 5; i++)
{
string name = string.Format("{0}_{1}", "btnAsync_Click", i);
action.BeginInvoke(name,null,null);
}
Console.WriteLine("************btnAsync_Click异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
watch.Stop();
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
异步方式出结果,如下所示:
通过对以上示例进行分析,得出结论如下:
异步方式不是顺序执行,即具有无序性。
异步方式采用多线程方式,和UI不是同一个线程,所以前端UI不会卡住。
异步多线程方式执行时间短,响应速度快。
通过观察任务管理器,发现同步方式比较耗时间,异步方式比较耗资源【本例是CPU密集型操作】,属于以资源换性能。同步方式和异步方式的CPU利用率,如下图所示:
异步多线程优化
通过上述例子,发现由于采用异步的原因,线程还未结束,但是排在后面的语句就先执行,所以统计的程序执行总耗时为0秒。为了优化此问题,采用async与await组合方式执行,代码如下所示:
private async void btnAsync2_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
Console.WriteLine("************btnAsync_Click2异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
await DoAsync();
Console.WriteLine("************btnAsync_Click2异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
watch.Stop();
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
/// <summary>
/// 异步方法
/// </summary>
/// <returns></returns>
private async Task DoAsync() {
Action<string> action = new Action<string>(DoSomethingLong);
List<IAsyncResult> results = new List<IAsyncResult>();
for (int i = 0; i < 5; i++)
{
string name = string.Format("{0}_{1}", "btnAsync_Click", i);
IAsyncResult result = action.BeginInvoke(name, null, null);
results.Add(result);
}
await Task.Run(()=> {
while (true)
{
for (int i = 0; i < results.Count; i++) {
var result = results[i];
if (result.IsCompleted) {
results.Remove(result);
break;
}
}
if (results.Count < 1) {
break;
}
Thread.Sleep(200);
}
});
}
经过优化,执行结果如下所示:
通过异步多线程优化后的执行结果,进行分析后得出的结论如下:
Action的BeginInvoke,会返回IAsyncResult接口,通过接口可以判断是否完成。
如果有多个Action的多线程调用,可以通过List方式进行。
async与await组合,可以实现异步调用,防止线程阻塞。
通过以上方式,采用异步多线程的方式,共耗时3.26秒,比同步方式的9.32秒,提高了2.85倍,并非线性增加。且每次执行的总耗时会上下浮动,并非固定值。
异步回调
上述async与await组合,是一种实现异步调用的方式,其实Action本身也具有回调函数【AsyncCallback】,通过回调函数一样可以实现对应功能。具体如下所示:
/// <summary>
/// 异步回调
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAsync3_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
Console.WriteLine("************btnAsync_Click3异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
Action action = DoAsync3;
AsyncCallback asyncCallback = new AsyncCallback((ar) =>
{
if (ar.IsCompleted)
{
Console.WriteLine("************btnAsync_Click3异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
watch.Stop();
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
});
action.BeginInvoke(asyncCallback, null);
}
private void DoAsync3()
{
Action<string> action = new Action<string>(DoSomethingLong);
List<IAsyncResult> results = new List<IAsyncResult>();
for (int i = 0; i < 5; i++)
{
string name = string.Format("{0}_{1}", "btnAsync_Click3", i);
IAsyncResult result = action.BeginInvoke(name, null, null);
results.Add(result);
}
while (true)
{
for (int i = 0; i < results.Count; i++)
{
var result = results[i];
if (result.IsCompleted)
{
results.Remove(result);
break;
}
}
if (results.Count < 1)
{
break;
}
Thread.Sleep(200);
}
}
异步回调执行示例,如下所示:
通过对异步回调方式执行结果进行分析,结论如下所示:
通过观察线程ID可以发现,由于对循环计算的功能进行了封装,为一个独立的函数,所以在Action通过BeginInvoke发起时,又是一个新的线程。
通过async和await在通过Task.Run方式返回时,也会重新生成新的线程。
通过回调函数,可以保证异步线程的执行顺序。
通过Thread.Sleep(200)的方式进行等待,会有一定时间范围延迟。
异步信号量
信号量方式是通过BeginInvoke返回值IAsyncResult中的异步等待AsyncWaitHandle触发信号WaitOne,可以实现信号的实时响应,具体代码如下:
private void btnAsync4_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
Console.WriteLine("************btnAsync_Click4异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
Action action = DoAsync3;
var asyncResult = action.BeginInvoke(null, null);
//此处中间可以做其他的工作,然后在最后等待线程的完成
asyncResult.AsyncWaitHandle.WaitOne();
Console.WriteLine("************btnAsync_Click4异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
watch.Stop();
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
信号量示例截图如下所示:
通过对异步信号量方式的测试结果进行分析,得出结论如下:
信号量方式会造成线程的阻塞,且会造成前端界面卡死。
信号量方式适用于异步方法和等待完成之间还有其他工作需要处理的情况。
WaitOne可以设置超时时间【最多可等待时间】。
异步多线程返回值
上述示例的委托都是无返回值类型的,那么对于有返回值的函数,如何获取呢?答案就是采用Func。示例如下所示:
private void btnAsync5_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
Console.WriteLine("************btnAsync5_Click异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
string name = string.Format("{0}_{1}", "btnAsync_Click5", 0);
Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn);
IAsyncResult asyncResult = func.BeginInvoke(name, null, null);
//此处中间可以做其他的工作,然后在最后等待线程的完成
int result = func.EndInvoke(asyncResult);
Console.WriteLine("************btnAsync5_Click异步方法 结束,线程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId,result);
watch.Stop();
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
private int DoSomethingLongAndReturn(string name)
{
Console.WriteLine("************DoSomethingLong 开始 name= {0} 线程ID= {1} 时间 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
//CPU计算累加和
long rest = 0;
for (int i = 0; i < 1000000000; i++)
{
rest += i;
}
Console.WriteLine("************DoSomethingLong 结束 name= {0} 线程ID= {1} 时间 = {2} 结果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
return DateTime.Now.Day;
}
采用Func方式的EndInvoke,可以获取返回值,示例如下:
通过对Func方式的EndInvoke方法的示例进行分析,得出结论如下所示:
在主线程中调用EndInvoke,会进行阻塞,前端页面卡死。
Func的返回值是泛型类型,可以返回任意类型的值。
异步多线程返回值回调
为了解决以上获取返回值时,前端页面卡死的问题,可以采用回调函数进行解决,如下所示:
private void btnAsync6_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
Console.WriteLine("************btnAsync6_Click异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
string name = string.Format("{0}_{1}", "btnAsync_Click6", 0);
Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn);
AsyncCallback callback = new AsyncCallback((asyncResult) =>
{
int result = func.EndInvoke(asyncResult);
Console.WriteLine("************btnAsync6_Click异步方法 结束,线程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId, result);
watch.Stop();
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
});
func.BeginInvoke(name, callback, null);
}
采用回调方式,示例截图如下:
通过对回调方式的示例进行分析,得出结论如下:
异步回调函数中调用EndInvoke,可以直接返回,不再阻塞。
异步回调方式,前端UI线程不再卡住。
来源:https://www.cnblogs.com/hsiang/p/15676795.html
猜你喜欢
- 序本文主要研究下迁移到java9的一些注意事项。迁移种类1、代码不模块化,先迁移到jdk9上,好利用jdk9的api2、代码同时也模块化迁移
- 公司有个业务需要查出所有的用户权限分类,并将最后一层类别所包含的权限查出来。数据库说明,有一个parent_id 字段是最好的:、paren
- 实现一个自定义的 @Conditional 派生注解自定义一个注解,继承 @Conditional 注解// 派生注解@Retention(
- * 与过滤器在讲Spring boot之前,我们先了解一下过滤器和 * 。这两者在功能方面很类似,但是在具体技术实现方面,差距还是比较大的
- 话不多说,先上图 &n
- 前言在真实的项目开发中,使用SpringBoot可以说非常普遍了,而在框架整合中,与数据库的交互无外乎使用jpa,mybatis,mybat
- WPF的InkCanvas就是一个画板,可以在上面随意涂鸦,每写上一笔,InkCanvas的Strokes集合里就新增一个涂鸦对象,下面的代
- CLR提供了可以区分类型的Equality 和Identity能力。Equality:如果两个对象是相同的类型,并且它们各自带有相同和等值的
- Java png图片修改像素rgba值import javax.imageio.ImageIO; import javax.swing.Im
- 前言《飞机大战-I》是一款融合了街机、竞技等多种元素的经典射击手游。华丽精致的游戏画面,超炫带感的技能特效,超火爆画面让你肾上腺素爆棚,给你
- 注解从java5开始加入这一特性,发展到现在已然是遍地开花,在很多框架中得到了广泛的使用,用来简化程序中的配置。那充满争议的类型注解究竟是什
- 前言Android提供了很多种保存应用程序数据的方法。其中一种就是用SharedPreferences对象来保存我们私有的键值(key-va
- 昨天有个刚学java的师弟发了个程序给我,说死活编译不过,老是报编码问题,自己试了一下,也出问题了...当我们编辑了一个Java源文件保存时
- 一、Servlet概述1.sun公司提供的动态web资源开发技术。本质是上一段java小程序,要求这个小程序必须实现Servlet接口,以便
- OOP语言的三大特征即:面向对象的三个比较重要的思想封装官话:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口进
- 整理文档,搜刮出一个java后台接受app上传的图片的示例代码,稍微整理精简一下做下分享package com.sujinabo.file;
- 1、心跳机制简介在分布式系统中,分布在不同主机上的节点需要检测其他节点的状态,如服务器节点需要检测从节点是否失效。为了检测对方节点的有效性,
- 环境Linux版本:CentOS 6.5、Ubuntu 12.04.5 JDK版本:JDK 1.7目录方法一:手动解压JDK的压缩包,然后设
- TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点。TreeSet 和 Tree
- AuthenticationProvider解析首先进入到AuthenticationProvider源码中可以看到它只是个简单的接口里面也