前言
在上一篇博文中, 我们提到了 APM 模型实现异步编程的模式, 通过使用 APM 模型, 可以简化. Net 中编写异步程序的方式, 但 APM 模型本身依然存在一些缺点, 如无法得知操作进度, 不能取消异步操作等.
针对这些缺点, 微软在. Net 2.0 中提出了基于事件的异步模式, 简称为 EAP 模型.
第二个异步编程模型: EAP
概述
EAP, 全称 Event-based Asynchronous Pattern, 基于事件的异步模式, 它提供了一系列的事件声明与方法, 用于实现异步模式的各个阶段.
典型的内置组件为 BackgroundWorker 组件, 本文中我们将使用它来探寻此种模式的执行过程.
使用
我们需要创建一个窗体应用, 并模拟下载实时进度显示. 创建 WinForm 后, 放入 Label 控件用于展示下载进度和其他信息, 并加入两个 Button 按钮, 分别为开始下载和取消下载, 再放入我们的主角: BackgroundWorker 组件, 如图所示:
在加入这些基本组件后, 我们开始这一次的编码之旅, BackgroundWorker 在后台属于一个类, 因此它已经内置了部分属性和事件:
这些属性中包含取消, 支持进度更新, 判断是否执行等, 恰恰是我们在这次异步操作中需要的. 于是, 我们根据需求编写了以下代码:
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Drawing;
- using System.IO;
- using System.Linq;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Windows.Forms;
- namespace BackgroundWorkerDemo
- {
- public partial class BackgroundWorkerForm : Form
- {
- public BackgroundWorkerForm()
- {
- InitializeComponent();
- backgroundWorker1.WorkerReportsProgress = true;
- backgroundWorker1.WorkerSupportsCancellation = true;
- }
- /// <summary>
- /// 点击开始下载按钮
- /// </summary>
- /// <param name="sender"></param>
- /// <param name="e"></param>
- private void btnDownLoad_Click(object sender, EventArgs e)
- {
- if (!backgroundWorker1.IsBusy) // 判断是否正在执行异步操作
- {
- //backgroundWorker 开始执行异步操作
- backgroundWorker1.RunWorkerasync();
- }
- }
- /// <summary>
- /// 点击取消按钮
- /// </summary>
- /// <param name="sender"></param>
- /// <param name="e"></param>
- private void btnCancel_Click(object sender, EventArgs e)
- {
- if (backgroundWorker1.WorkerSupportsCancellation) // 判断是否支持异步取消操作
- {
- // 开始执行取消操作
- backgroundWorker1.CancelAsync();
- }
- }
- /// <summary>
- /// backgroundworker 异步执行事件
- /// </summary>
- /// <param name="sender"></param>
- /// <param name="e"></param>
- private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
- {
- System.ComponentModel.BackgroundWorker worker = sender as System.ComponentModel.BackgroundWorker;
- string msg = "当前线程是否为后台线程:" + Thread.CurrentThread.IsBackground + ", 是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread;
- WriteLog("Backgroundworker 日志", msg);
- for (int i = 0; i <20; i++)
- {
- if (worker.CancellationPending)
- {
- e.Cancel = true;
- break;
- }
- else
- {
- // 模拟下载执行进度
- Thread.Sleep(500);
- worker.ReportProgress(i * 5);
- }
- }
- }
- /// <summary>
- /// 进度报告事件
- /// </summary>
- /// <param name="sender"></param>
- /// <param name="e"></param>
- private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
- {
- lblProcess.Text = "当前下载进度为:" + e.ProgressPercentage + "%, 是否为后台线程:" + Thread.CurrentThread.IsBackground + ", 是否为线程池线程:" + Thread.CurrentThread.IsThreadPoolThread;
- }
- /// <summary>
- /// 异步操作完成事件
- /// </summary>
- /// <param name="sender"></param>
- /// <param name="e"></param>
- private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- if (e.Cancelled) // 此状态为取消
- {
- lblProcess.Text = "下载已经被取消";
- }
- else if (e.Error != null)
- {
- lblProcess.Text = "出现错误:" + e.Error.Message;
- }
- else
- {
- lblProcess.Text = "下载已完成";
- }
- }
- /// <summary>
- /// 记录日志
- /// </summary>
- /// <param name="documentName"></param>
- /// <param name="msg"></param>
- public void WriteLog(string documentName, string msg)
- {
- string errorLogFilePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
- if (!System.IO.Directory.Exists(errorLogFilePath))
- {
- System.IO.Directory.CreateDirectory(errorLogFilePath);
- }
- string logFile = System.IO.Path.Combine(errorLogFilePath, documentName + "@" + DateTime.Today.ToString("yyyy-MM-dd") + ".txt");
- bool writeBaseInfo = System.IO.File.Exists(logFile);
- StreamWriter swLogFile = new StreamWriter(logFile, true, Encoding.Unicode);
- swLogFile.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "\t" + msg);
- swLogFile.Close();
- swLogFile.Dispose();
- }
- }
- }
在这段示例代码中, 我们首先设置组件支持取消及报告进度操作属性, 其次在点击开始按钮时, 判断是否执行, 若未执行, 则执行 RunWorkerAsync 方法, 避免多次重复执行.
在 EAP 模型中, 执行 RunWorkerAsync 方法后, 会触发 backgroundWorker1_DoWork 事件. 此事件中我们放入模拟实时下载进度代码, 并调用 ReportProgress 进行进度报告, 这时 backgroundWorker1_ProgressChanged 事件会被触发, 同时对 UI 进行更新操作, 此段过程运行结果如下图所示:
通过结果可以看出, 运行过程中已经实现了实时更新进度的功能. 于此同时, 根据反馈的信息我们发现, backgroundWorker1_ProgressChanged 事件内部是线程安全的, 在操作 UI 时不会出现跨线程对 UI 进行更新的问题.
那么 BackgroundWorker 内部是不是依然使用了线程池及后台线程呢? 我们来一起看看在 backgroundWorker1_DoWork 事件中记录的日志:
通过日志我们发现, EAP 与 APM 一样, 也使用了线程池中的线程, 不得不感叹一句, 线程池是个伟大的发明, 微软真是无所不用其极啊!
讲到这里, 细心的同学会发现, 我们唠叨了这么半天, 似乎还少了点什么, 对了, 取消操作, 一起来看看效果:
点击界面上的 "取消下载" 按钮后, 会提示下载已经被取消. 原因是我们在点击按钮时, 首先判断了 WorkerSupportsCancellation 属性, 看组件是否支持取消操作, 随后执行 CancelAsync 方法进行异步取消.
由于这个过程是异步的, 因此我们在 backgroundWorker1_DoWork 事件中不断判断 CancellationPending 属性, 若取消则设置 e.Cancel=true 进行标志位标志, 标志后我们可以在 backgroundWorker1_RunWorkerCompleted 判断是否已经取消, 最后对 UI 进行提示输出, 取消操作完成.
小结
对比 APM 调用委托进行异步操作的方式, EAP 显得更加简洁明了, 只需更少的代码即可实现更多的功能. 尤其是 BackgroundWorker 组件, 定义相应的事件后, 在不同阶段根据需求编写方法即可实现异步操作, 报告进度及取消等.
但是 EAP 模型的使用, 局限性会更强, 主要包括以下几点:
1, 可用组件少, 除了 BackgroundWorker 之外, 仅有 webClient 类支持此模型, 在 B/S 程序中难以使用
2, 只能使用预定义事件, 无法手动定义回调函数, 且依赖事件的执行顺序
3, 内部封装较多, 占用资源比 APM 方式多
因此在愈演愈烈的需求中, 微软又对异步编程模型进行了变革, 一种兼顾强大与灵活的新模型诞生了, 它会是谁呢? 预知后事如何, 且听下回分解.
下集预告
浅谈. Net 异步编程的前世今生 ----TPL 篇
来源: https://www.cnblogs.com/wackysoft/p/10927588.html