C# 多线程入门 - 开发实例


C# 多线程入门 - 开发实例

线程(英语: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);
 
 }

同步方式输出结果,如下所示:

C# 多线程入门系列(一)

通过对以上示例进行分析,得出结论如下:

  1. 同步方式按顺序依次执行。
  2. 同步方式业务和UI采用采用同一线程,都是主线程。
  3. 同步方式如果执行操作比较耗时,前端UI会卡住,无法响应用户请求。
  4. 同步方式比较耗时【本示例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"));
 }

异步方式出结果,如下所示:

C# 多线程入门系列(一)

通过对以上示例进行分析,得出结论如下:

  1. 异步方式不是顺序执行,即具有无序性。
  2. 异步方式采用多线程方式,和UI不是同一个线程,所以前端UI不会卡住。
  3. 异步多线程方式执行时间短,响应速度快。

通过观察任务管理器,发现同步方式比较耗时间,异步方式比较耗资源【本例是CPU密集型操作】,属于以资源换性能。同步方式和异步方式的CPU利用率,如下图所示:

C# 多线程入门系列(一)

异步多线程优化

通过上述例子,发现由于采用异步的原因,线程还未结束,但是排在后面的语句就先执行,所以统计的程序执行总耗时为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);
     
         }
     });
     
 }

经过优化,执行结果如下所示:

C# 多线程入门系列(一)

通过异步多线程优化后的执行结果,进行分析后得出的结论如下:

  1. Action的BeginInvoke,会返回IAsyncResult接口,通过接口可以判断是否完成。
  2. 如果有多个Action的多线程调用,可以通过List方式进行。
  3. async与await组合,可以实现异步调用,防止线程阻塞。
  4. 通过以上方式,采用异步多线程的方式,共耗时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);
 
     }
 }

异步回调执行示例,如下所示:

C# 多线程入门系列(一)

通过对异步回调方式执行结果进行分析,结论如下所示:

  1. 通过观察线程ID可以发现,由于对循环计算的功能进行了封装,为一个独立的函数,所以在Action通过BeginInvoke发起时,又是一个新的线程。
  2. 通过async和await在通过Task.Run方式返回时,也会重新生成新的线程。
  3. 通过回调函数,可以保证异步线程的执行顺序。
  4. 通过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"));
 }

信号量示例截图如下所示:

C# 多线程入门系列(一)

 

通过对异步信号量方式的测试结果进行分析,得出结论如下:

  1. 信号量方式会造成线程的阻塞,且会造成前端界面卡死。
  2. 信号量方式适用于异步方法和等待完成之间还有其他工作需要处理的情况。
  3. 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,可以获取返回值,示例如下:

C# 多线程入门系列(一)

 

通过对Func方式的EndInvoke方法的示例进行分析,得出结论如下所示:

  1. 在主线程中调用EndInvoke,会进行阻塞,前端页面卡死。
  2. 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);
 }

采用回调方式,示例截图如下:

C# 多线程入门系列(一)

 

通过对回调方式的示例进行分析,得出结论如下:

  1. 异步回调函数中调用EndInvoke,可以直接返回,不再阻塞。
  2. 异步回调方式,前端UI线程不再卡住。

备注

以上就是通过Action和Func的对应BeginInvoke方法实现异步多线程的示例,旨在抛砖引玉,大家共同学习,一起进步。

 

版权声明:本文为开发框架文库发布内容,转载请附上原文出处连接
C/S框架网
上一篇:.NET Core 集成Swagger文档与自定义Swagger UI
下一篇:C# VS自带的Chart图表控件实现实时折线图,波形图
评论列表

发表评论

评论内容
昵称:
关联文章

C# 线入门 - 开发实例
C#异步编程(线
C#,Asp.Net线断点续传下载
C#线处理个队列的数据(交叉线访问及Invoke方法使用)
C#.Net使用线池(ThreadPool)与专用线(Thread)
C#异步操作等待窗体,异步线处理数据通用界面(frmThreadOperating)
C#线异步处理数据通用界面窗体(frmThreadOperating)
C#线使用读写锁ReaderWriterLockSlim同步写入文件
C#.NET7 线播放mp3/wav音频|winmm.dll winapi播放音乐文件
.NET5 HttpClient线并发请求阻塞“发生一个或个错误”解决方案
C#.Net前台线与后台线的区别
尝试释放正在使用的RCW,活动线或其他线上正在使用该RCW
C#.NET 后端WebApi接口搭建教,WebApi接口开发实例
C#启动程序时检测运行实例
Winform三层架构教,CS三层结构图及源码实例讲解
CSFramework.WebApi新手入门实战开发教程(C#+VS2017)
Form - 生产计划排窗体C#源码
C# EntityFrameworkCore EF Core 入门使用
C# .NET 入门概念与知识点总结
C#.NET MVC WebApi后台开发框架入门完整版下载

热门标签
.NET5 .NET6 .NET7 APP Auth-软件授权注册系统 Axios B/S B/S开发框架 Bug Bug记录 C#加密解密 C#源码 C/S CHATGPT CMS系统 CodeGenerator CSFramework.DB CSFramework.EF CSFrameworkV1学习版 CSFrameworkV2标准版 CSFrameworkV3高级版 CSFrameworkV4企业版 CSFrameworkV5旗舰版 CSFrameworkV6.0 DAL数据访问层 Database datalock DbFramework Demo教学 Demo下载 DevExpress教程 DOM EF框架 Element-UI EntityFramework ERP ES6 Excel FastReport GIT HR IDatabase IIS JavaScript LINQ MES MiniFramework MIS NavBarControl Node.JS NPM OMS ORM PaaS POS Promise API Redis SAP SEO SQL SQLConnector TMS系统 Token令牌 VS2022 VSCode VUE WCF WebApi WebApi NETCore WebApi框架 WEB开发框架 Windows服务 Winform 开发框架 Winform 开发平台 WinFramework Workflow工作流 Workflow流程引擎 版本区别 报表 踩坑日记 操作手册 代码生成器 迭代开发记录 基础资料窗体 架构设计 角色权限 开发sce 开发技巧 开发教程 开发框架 开发平台 开发指南 客户案例 快速搭站系统 快速开发平台 秘钥 密钥 权限设计 软件报价 软件测试报告 软件简介 软件开发框架 软件开发平台 软件开发文档 软件体系架构 软件下载 软著证书 三层架构 设计模式 生成代码 实用小技巧 收钱音箱 数据锁 数据同步 微信小程序 未解决问题 文档下载 喜鹊ERP 喜鹊软件 系统对接 详细设计说明书 行政区域数据库 需求分析 疑难杂症 蝇量级框架 蝇量框架 用户管理 用户开发手册 用户控件 在线支付 纸箱ERP 智能语音收款机 自定义窗体 自定义组件 自动升级程序