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


线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源或正在使用它们。本文以一些简单的小例子,简述如何将程序由同步方式,一步一步演变成异步多线程方式,仅供学习分享使用,如有不足之处,还请指正。

同步方式

业务场景:用户点击一个按钮,然后做一个耗时的业务。同步方式代码如下所示:

 1 private void btnSync_Click(object sender, EventArgs e)
 2 {
 3     Stopwatch watch = Stopwatch.StartNew();
 4     watch.Start();
 5     Console.WriteLine("************btnSync_Click同步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
 6     for (int i = 0; i < 5; i++)
 7     {
 8         string name = string.Format("{0}_{1}", "btnSync_Click", i);
 9         this.DoSomethingLong(name);
10     }
11     Console.WriteLine("************btnSync_Click同步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
12     watch.Stop();
13     Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
14 }
15 
16 
17 /// <summary>
18 /// 模拟做一些长时间的工作
19 /// </summary>
20 /// <param name="name"></param>
21 private void DoSomethingLong(string name)
22 {
23     Console.WriteLine("************DoSomethingLong 开始 name= {0} 线程ID= {1} 时间 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
24     //CPU计算累加和
25     long rest = 0;
26     for (int i = 0; i < 1000000000; i++)
27     {
28         rest += i;
29     }
30     Console.WriteLine("************DoSomethingLong 结束 name= {0} 线程ID= {1} 时间 = {2} 结果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
31 
32 }

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

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

  1. 同步方式按顺序依次执行。
  2. 同步方式业务和UI采用采用同一线程,都是主线程。
  3. 同步方式如果执行操作比较耗时,前端UI会卡住,无法响应用户请求。
  4. 同步方式比较耗时【本示例9.32秒】

异步多线程方式

如何优化同步方式存在的问题呢?答案是由同步方式改为异步异步多线程方式。代码如下所示:

 1 private void btnAsync_Click(object sender, EventArgs e)
 2 {
 3     Stopwatch watch = Stopwatch.StartNew();
 4     watch.Start();
 5     Console.WriteLine("************btnAsync_Click异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
 6     Action<string> action = new Action<string>(DoSomethingLong);
 7     for (int i = 0; i < 5; i++)
 8     {
 9         string name = string.Format("{0}_{1}", "btnAsync_Click", i);
10         action.BeginInvoke(name,null,null);
11     }
12     Console.WriteLine("************btnAsync_Click异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
13     watch.Stop();
14     Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
15 }

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

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

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

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

 异步多线程优化

通过上述例子,发现由于采用异步的原因,线程还未结束,但是排在后面的语句就先执行,所以统计的程序执行总耗时为0秒。为了优化此问题,采用async与await组合方式执行,代码如下所示:

 1 private async void btnAsync2_Click(object sender, EventArgs e)
 2 {
 3     Stopwatch watch = Stopwatch.StartNew();
 4     watch.Start();
 5     Console.WriteLine("************btnAsync_Click2异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
 6     await DoAsync();
 7     Console.WriteLine("************btnAsync_Click2异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
 8     watch.Stop();
 9     Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
10 }
11 
12 /// <summary>
13 /// 异步方法
14 /// </summary>
15 /// <returns></returns>
16 private async Task DoAsync() {
17     Action<string> action = new Action<string>(DoSomethingLong);
18     List<IAsyncResult> results = new List<IAsyncResult>();
19     for (int i = 0; i < 5; i++)
20     {
21         string name = string.Format("{0}_{1}", "btnAsync_Click", i);
22         IAsyncResult result = action.BeginInvoke(name, null, null);
23         results.Add(result);
24     }
25     await Task.Run(()=> { 
26         while (true)
27         {
28             for (int i = 0; i < results.Count; i++) {
29                 var result = results[i];
30                 if (result.IsCompleted) {
31                     results.Remove(result);
32                     break;
33                 }
34             }
35             if (results.Count < 1) {
36                 break;
37             }
38             Thread.Sleep(200);
39     
40         }
41     });
42     
43 }

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

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

  1. Action的BeginInvoke,会返回IAsyncResult接口,通过接口可以判断是否完成。
  2. 如果有多个Action的多线程调用,可以通过List方式进行。
  3. async与await组合,可以实现异步调用,防止线程阻塞。
  4. 通过以上方式,采用异步多线程的方式,共耗时3.26秒,比同步方式的9.32秒,提高了2.85倍,并非线性增加。且每次执行的总耗时会上下浮动,并非固定值。

异步回调

 上述async与await组合,是一种实现异步调用的方式,其实Action本身也具有回调函数【AsyncCallback】,通过回调函数一样可以实现对应功能。具体如下所示:

 1 /// <summary>
 2 /// 异步回调
 3 /// </summary>
 4 /// <param name="sender"></param>
 5 /// <param name="e"></param>
 6 private void btnAsync3_Click(object sender, EventArgs e)
 7 {
 8     Stopwatch watch = Stopwatch.StartNew();
 9     watch.Start();
10     Console.WriteLine("************btnAsync_Click3异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
11     Action action = DoAsync3;
12     AsyncCallback asyncCallback = new AsyncCallback((ar) =>
13     {
14         if (ar.IsCompleted)
15         {
16             Console.WriteLine("************btnAsync_Click3异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
17             watch.Stop();
18             Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
19         }
20     });
21     action.BeginInvoke(asyncCallback, null);
22 
23 }
24 
25 private void DoAsync3()
26 {
27     Action<string> action = new Action<string>(DoSomethingLong);
28     List<IAsyncResult> results = new List<IAsyncResult>();
29     for (int i = 0; i < 5; i++)
30     {
31         string name = string.Format("{0}_{1}", "btnAsync_Click3", i);
32         IAsyncResult result = action.BeginInvoke(name, null, null);
33         results.Add(result);
34     }
35     
36     while (true)
37     {
38         for (int i = 0; i < results.Count; i++)
39         {
40             var result = results[i];
41             if (result.IsCompleted)
42             {
43                 results.Remove(result);
44                 break;
45             }
46         }
47         if (results.Count < 1)
48         {
49             break;
50         }
51         Thread.Sleep(200);
52 
53     }
54 }

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

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

  1. 通过观察线程ID可以发现,由于对循环计算的功能进行了封装,为一个独立的函数,所以在Action通过BeginInvoke发起时,又是一个新的线程。
  2. 通过async和await在通过Task.Run方式返回时,也会重新生成新的线程。
  3. 通过回调函数,可以保证异步线程的执行顺序。
  4. 通过Thread.Sleep(200)的方式进行等待,会有一定时间范围延迟。

 异步信号量

信号量方式是通过BeginInvoke返回值IAsyncResult中的异步等待AsyncWaitHandle触发信号WaitOne,可以实现信号的实时响应,具体代码如下:

 1 private void btnAsync4_Click(object sender, EventArgs e)
 2 {
 3     Stopwatch watch = Stopwatch.StartNew();
 4     watch.Start();
 5     Console.WriteLine("************btnAsync_Click4异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
 6     Action action = DoAsync3;
 7     var asyncResult = action.BeginInvoke(null, null);
 8     //此处中间可以做其他的工作,然后在最后等待线程的完成
 9     asyncResult.AsyncWaitHandle.WaitOne();
10     Console.WriteLine("************btnAsync_Click4异步方法 结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
11     watch.Stop();
12     Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
13 }

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

 

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

  1. 信号量方式会造成线程的阻塞,且会造成前端界面卡死。
  2. 信号量方式适用于异步方法和等待完成之间还有其他工作需要处理的情况。
  3. WaitOne可以设置超时时间【最多可等待时间】。

异步多线程返回值

上述示例的委托都是无返回值类型的,那么对于有返回值的函数,如何获取呢?答案就是采用Func。示例如下所示:

 1 private void btnAsync5_Click(object sender, EventArgs e)
 2 {
 3     Stopwatch watch = Stopwatch.StartNew();
 4     watch.Start();
 5     Console.WriteLine("************btnAsync5_Click异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
 6     string name = string.Format("{0}_{1}", "btnAsync_Click5", 0);
 7     Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn);
 8     IAsyncResult asyncResult = func.BeginInvoke(name, null, null);
 9     //此处中间可以做其他的工作,然后在最后等待线程的完成
10     int result = func.EndInvoke(asyncResult);
11     Console.WriteLine("************btnAsync5_Click异步方法 结束,线程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId,result);
12     watch.Stop();
13     Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
14 }
15 
16 private int DoSomethingLongAndReturn(string name)
17 {
18     Console.WriteLine("************DoSomethingLong 开始 name= {0} 线程ID= {1} 时间 = {2}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
19     //CPU计算累加和
20     long rest = 0;
21     for (int i = 0; i < 1000000000; i++)
22     {
23         rest += i;
24     }
25     Console.WriteLine("************DoSomethingLong 结束 name= {0} 线程ID= {1} 时间 = {2} 结果={3}************", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"), rest);
26     return DateTime.Now.Day;
27 }

采用Func方式的EndInvoke,可以获取返回值,示例如下:

 

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

  1. 在主线程中调用EndInvoke,会进行阻塞,前端页面卡死。
  2. Func的返回值是泛型类型,可以返回任意类型的值。

异步多线程返回值回调

为了解决以上获取返回值时,前端页面卡死的问题,可以采用回调函数进行解决,如下所示:

 1 private void btnAsync6_Click(object sender, EventArgs e)
 2 {
 3     Stopwatch watch = Stopwatch.StartNew();
 4     watch.Start();
 5     Console.WriteLine("************btnAsync6_Click异步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
 6     string name = string.Format("{0}_{1}", "btnAsync_Click6", 0);
 7     Func<string, int> func = new Func<string, int>(DoSomethingLongAndReturn);
 8     AsyncCallback callback = new AsyncCallback((asyncResult) =>
 9     {
10         int result = func.EndInvoke(asyncResult);
11         Console.WriteLine("************btnAsync6_Click异步方法 结束,线程ID= {0},返回值={1}************", Thread.CurrentThread.ManagedThreadId, result);
12         watch.Stop();
13         Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
14     });
15     func.BeginInvoke(name, callback, null);
16 }

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

 

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

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

备注

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

菩萨蛮·书江西造口壁

【作者】辛弃疾 【朝代】宋

郁孤台下清江水,中间多少行人泪?西北望长安,可怜无数山。

青山遮不住,毕竟东流去。江晚正愁余,山深闻鹧鸪。

文章来源:https://www.cnblogs.com/hsiang/p/15676795.html

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:从一次解决Nancy参数绑定“bug”开始发布自己的第一个nuget包(上篇)
下一篇:[WPF] 用 OpacityMask 模仿 UWP 的 Text Shimmer 动画
评论列表

发表评论

评论内容
昵称:
验证码:
验证码
关联文章

C# 线入门系列
C# 线入门系列(三)
C# 线入门系列(二)
C# List<T>线安全
C# Abp框架入门系列文章(
SQLite Database 线访问需要注意的问题
Python线中试用wmi报错
C#线下载图片 URL转Image
.Net 线安全集合
ManualResetEvent实现线的暂停与恢复
C#使用Thrift作为RPC框架入门()
次本地正常上线接口报404
话说C#程序员人手个ORM
ASP.NET MVC快速入门
Python退出主进程后子线不会退出的解决方案
C# Socket网络编程 系列课程
Elasticsearch使用系列-ES简介和环境搭建
Windows下Git账号配置,同电脑个ssh-key的管理
使用 WPF 做个 PowerPoint 系列 基于 OpenXML 解析实现 PPT 文本描边效果
次 .NET 某化妆品 webapi 卡死分析

热门标签
.NET Core .NET Reactor ag-grid AI发布 api安全 ASP.NET Core C#DLL加密 C#播放声音 C#代码混淆 C#代码加密 ChromeDriver Codex DateTime DBeaver devexpress devTool DLL混淆 edge.js EF EFCore Electron element-ui el-form el-table excel FastReport FileStream FolderBrowerDialog FolderSelectDialog form提交 git gridcontrol gridview input javascript json字符串 JS转换对象JSON jwt JWT授权 linq log Math MCP mitmproxy MVC MySQL Navicat netstat nginx node_modules NSwag Nuget Nuget镜像 number PowerShell pyinstaller python pythoncom python爬虫 python抓包 pywin32 redis Requests-html RestSharp Selenium sql SQL Server Swagger to-cms Visual Studio VSCode vue VueRouter vue路由 VUE页面通讯 Webpack Windows Windows服务 winform wmi xlrd yaml YESCMS YESWEB开发框架 白象 表单提交 播放声音 打开URL 代码混淆 弹窗提醒 端口占用 对象转换 分布式 公共字典 机器码 进程排查 静态资源 开发指南 路由参数 密钥 配置教程 配置文件 权限 人工智能 任务 任务调度 日期间隔 日志 日志记录 省市区 授权验证 数据库 四舍五入 文案 文件读取 文件夹选择 文件目录选择 问题排查 行政区域数据 页面通讯 中间件 CSharp 事务锁 工单系统 并发控制 重复提交 CMS Markdig Markdown markdown-it marked 技术选型 VS Code 开发工具 源代码管理 版本控制 Docker PostgreSQL 时区 部署排查 CMS架构 EF Core 主题系统 二次开发 插件系统 容器 运维命令 镜像清理 Linux NAS 远程挂载 飞牛 fnOS S/4HANA SAP GUI SAP HANA SAP R/3 SAP入门 SAP版本 ERP SAP SAP MM 库存管理 物料管理 采购管理 入门教程 SAP S/4HANA SPRO 企业结构 采购组织 MM01 物料主数据 物料类型 BP分组 业务伙伴 供应商主数据 ME41 RFQ 库存物料 采购流程 ME51 消耗性物料 科目分配 采购申请 AC03 ML81N 外部服务 服务主数据 Business Partner SAP培训 ME51N MM模块 Lean Services MM-SRV 外部服务采购 PIR 供应来源 采购主数据 采购信息记录 ME31K 框架协议 计划协议 采购合同 ME01 供应来源确定 货源清单 MEQ1 供应源确定 配额安排 配额评分 MD04 MD21 MRP 计划文件 需求计划 批量程序 MD01N MD02 MRP Live MD05 MM 物料计划 优化采购 供应源 采购订单 ME2A 供应商确认 采购监控 Flexible Workflow 凭证释放 采购审批 释放策略 实地盘点 物料凭证 货物移动 MIGO 收货 移动类型 已撤回 供应商退货 货物发出 STO 库存转储 转移过账 生产订单 预留 GR/IR MIRO 供应商发票 物流发票校验 OMR2 税码 FI PP SD 实操教程 MRBR OMR6 发票差异 交货成本 后续借记 MI01 实物盘点 盘点差异 公司代码 工厂 组织结构 OMS2 主数据定制 自动科目确定 BP角色 CVI 伙伴确定 编号范围 凭证类型 字段选择 FBN1 OMBT OMC2 会计凭证 OMJJ BOM 委外加工 项目类别L MRKO 供应商寄售 特殊库存K MRKON PIPE Pipeline 特殊库存P ERS MRIS 发票计划 周期性结算 里程碑付款 变更追踪 版本管理 采购凭证 SFTP WebDAV 网盘 飞牛fnOS AMPL HERS MPN 中文教程 库存确定 可用性检查 缺件检查 Output Management 消息确定 输出确定 分割评估 库存计价 评估类别 评估类型 PB00 RM0000 条件技术 采购定价 MM-FI集成 OBYC 库存估价 文本类型 文本采用 EFB EVO MSV SU3 用户参数 发票校验 合同参照 履约保留款 特别总账 预付款 Fiori Launchpad SAP Fiori 应用导航 用户体验 LSMW LTMC Migration Cockpit 数据迁移 BRFplus OPD Output Control My Inbox 审批流程 灵活工作流 SAP PP 外部加工 SAP QM 检验批 质量信息记录 采购收货 SAP PM 维护BOM 维护订单 SAP SD SAP Service 端到端流程 MM模块培训 FI-MM集成 供应商管理 审批配置 FICO入门 SAP FICO 财务配置 供应商税务 预扣税 House Bank 银行对账 客户清账 应收账款 FI控制 验证与替代 印度 GST 税务配置 F110 FBZP EWM入门 SAP EWM 仓库管理 OX14 成本核算 物料评估 后勤配置 物料组 价值更新 数量更新 PP-PI 流程制造 生产计划 容差配置 SAP事务码 SAP基础 TCODE Basis 事务代码 MMNR 编号区间 采购实操 组织架构 OMSF SAP实操 FI配置
联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码