[你必须知道的异步编程]——基于事件的异步编程模式
本专题概要:
- 引言
- 你听说过EAP吗?——基于事件异步编程模式介绍
- 深入剖析BackgroundWorker组件类
- 使用BackgroundWorker组件进行异步编程
- 小结
一、引言
在上一个专题中为大家介绍了.NET 1.0中提出来的异步编程模式——APM,虽然APM为我们实现异步编程提供了一定的支持,同时它也存在着一些明显的问题——不支持对异步操作的取消和没有提供对进度报告的功能,对于有界面的应用程序来说,进度报告和取消操作的支持也是必不可少的,既然存在这样的问题,微软当然也应该提供给我们解决问题的方案了,所以微软在.NET 2.0的时候就为我们提供了一个新的异步编程模型,也就是我这个专题中介绍的基于事件的异步编程模型——EAP。下面就为大家全面介绍了这个异步编程模型。
二、你听说过EAP吗?——基于事件异步编程模式介绍
对于一些朋友可能在平时的工作和学习中已经接触到了基于事件的异步编程模式的,只是大家可能不知道它使用了异步编程模式罢了(首先我就是这样的,之前很早就用过BackgroundWorker做过一个小程序,但是不知道该组件是基于事件的异步编程模式),也可能一些朋友之前没有接触过基于事件的异步编程模式的,现在就为大家具体介绍下这个在.NET 2.0中提出来的新的异步编程模式。
实现了基于事件的异步模式的类将具有一个或者多个以Async为后缀的方法和对应的Completed事件,并且这些类都支持异步方法的取消、进度报告和报告结果。 然而在.NET类库中并不是所有的类都支持EAP的,可能有朋友会误认为是不是支持APM的类都支持EAP的呢?在.NET 类库中只有部分的类支持EAP的(并且也只有部分类支持APM),这些类有(共17个类):
System.Object的派生类型:
System.Activies.WorkflowInvoke
System.Deployment.Application.ApplicationDeployment
System.Deployment.Application.InPlaceHosingManager
System.Net.Mail.SmtpClient
System.Net.PeerToPeer.PeerNameResolver
System.Net.PeerToPeer.Collaboration.ContactManager
System.Net.PeerToPeer.Collaboration.Peer
System.Net.PeerToPeer.Collaboration.PeerContact
System.Net.PeerToPeer.Collaboration.PeerNearMe
System.ServiceModel.Activities.WorkflowControlClient
System.ServiceModel.Discovery.AnnoucementClient
System.ServiceModel.Discovery.DiscoveryClient
System.ComponentModel.Component的派生类型:
System.ComponentModel.BackgroundWorker
System.Net.NetworkInformation.Ping
System.Windows.Forms.PictureBox(继承于Control类,Control类派生于Component类)
当我们调用实现基于事件的异步模式的类的 XxxAsync方法时,即代表开始了一个异步操作,该方法调用完之后会使一个线程池线程去执行耗时的操作,所以当UI线程调用该方法时,当然也就不会堵塞UI线程了。并且基于事件的异步模式是建立了APM的基础之上的(这也是我在上一专题中详解介绍APM的原因),而APM又是建立了在委托之上的(对于这点可以参考该系列的APM专题)。然而这点并不是凭空想象的,下面就BackgroundWorker类来给大家详解解释EAP是建立在APM的基础上的。
三、深入剖析BackgroundWorker组件类
在深入讲解BackgroundWorker类之前,让我们先看看BackgroundWorker类具有的成员和对应的介绍的(这里只列出一些在异步编程中经常使用的属性和方法,具体关于该类成员可以查看MSDN——BackgroundWorker):
BackgroundWorker类 |
|
公共属性 |
|
属性名 |
说明 |
CancellationPending |
获取一个值,指示应用程序是否已请求取消后台操作 |
IsBusy |
获取一个值,指示 BackgroundWorker 是否正在运行异步操作。 |
WorkReportsProgress |
获取或设置一个值,该值指示 BackgroundWorker 能否报告进度更新。 |
WorkerSupportsCancellation |
获取或设置一个值,该值指示 BackgroundWorker 是否支持异步取消。 |
公共方法 |
|
名称 |
说明 |
CancelAsync |
请求取消挂起的后台操作。 |
ReportProgress |
引发 ProgressChanged 事件(官方这样解释我就要信?) |
RunWorkerAsync |
开始执行后台操作。 |
公共事件 |
|
名称 |
说明 |
DoWork |
调用 RunWorkerAsync 时发生(官方是这么解释的,你想知道为什么调用RunWorkerAsync方法就会触发DoWork事件吗?) |
ProgressChanged |
调用ReportProgress时发生(官方是这么解释的,你想知道为什么调用ReportProgress方法就会触发ProgressChanged事件吗?) |
RunWorkerCompleted |
当后台操作已完成、被取消或引发异常时发生。 |
在上表中首先提出了我的疑问,官方解释当我们调用RunWorkerAsync方法时就会触发DoWork事件,调用ReportProgress方法就会触发ProgressChanged事件,这里我就想探究下为什么会这样的,为了探究BackgroundWorker类背后的故事,这里当然就少不了使用反射工具Reflector来查看它的源码了,现在就进入我们的分析过程。
首先就来分析为什么调用RunWorkerAsync方法就会触发DoWorker事件? 首先我们看看RunWorkerAsync方法的源码是如何的:
// RunWorkerAsync的源码什么都没有做,只是调用了该方法的重载方法RunWorkerAsync(object argument)方法 public void RunWorkerAsync() { this.RunWorkerAsync(null); } // 下面就看看RunWorkerAsync带有一个参数的重载方法的源码 public void RunWorkerAsync(object argument) { if (this.isRunning) { throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerAlreadyRunning")); } // 这个方法把一些私有字段赋值 // 这些赋值是为了我们使用isBusy公共属性来检查BackgroundWorker组件是否在运行异步操作 // 和检查公共属性 CancellationPending属性来检查异步操作是否取消 this.isRunning = true; this.cancellationPending = false; // AsyncOperation类是通过获得调用线程的同步上下文来实现跨线程访问,这个实现在APM专题中我们是自己通过代码来实现的,然而实现EAP的类在内容帮我们实现了,这样就不需要我们自己去解决这个问题了,从中也可以看出EAP的实现是基于APM的,只是实现EAP的类帮我们做了更多的背后的事情 this.asyncOperation = AsyncOperationManager.CreateOperation(null); // 这里就是我们上一专题中介绍的使用委托实现的异步编程部分 // 我们在EAP的类中调用了BeginInvoke方法,从而也可以证明EAP是基于APM的,所以APM的介绍很有必要。 this.threadStart.BeginInvoke(argument, null, null); }
从上面的代码中可以证明本专题开始说的 “EAP是基于APM”并不是我凭空想象出来的,而是实现EAP的类确实也是这么做的,虽然这个假设已经得到证明了,然而从上面的代码还是不知道解释调用了RunWorkerAsync方法就会触发DoWork事件的发生啊? 对于这个疑惑,我很快会为大家明白,这一切的一切又是委托在起作用了(所以我一直认为,微软的几乎所有特性都是基于委托来实现的,然而委托又是方法的包装,具体可以参看的委托专题. 从而又追根到底就是方法了)。
- 我们从上面的代码可以看到调用RunWorkerAsync方法就是调用threadStart委托,我们要知道RunWorkerAsync方法到底背后发生了什么事情,就首先需要知道threadStart委托包装了哪个方法?并且需要知道委托在什么地方实例化的?
- 委托什么地方实例化话的?谈到实例化当然大家首先想到的就是构造函数了,不错,我们就看看BackgroundWorker构造函数:
// 这里查看构造函数都是因为前面的分析 // 从构造函数中我们可以确实可以看到threadStart委托是这里初始化的 public BackgroundWorker() { // 初始化threadStart委托 this.threadStart = new WorkerThreadStartDelegate(this.WorkerThreadStart); // 这里也初始化了操作完成委托和进度报告委托 this.operationCompleted = new SendOrPostCallback(this.AsyncOperationCompleted); this.progressReporter = new SendOrPostCallback(this.ProgressReporter); }
3. 从构造函数中已经知道threadStart包装了WorkerThreadStart方法,从而解决了第一步的疑惑,接下来就让我们看看WorkerThreadStart方法的代码:
private void WorkerThreadStart(object argument) { object result = null; Exception error = null; bool cancelled = false; try { DoWorkEventArgs e = new DoWorkEventArgs(argument); // 该方法中又是调用了onDoWork方法 // this.OnDoWork(e); if (e.Cancel) { cancelled = true; } else { result = e.Result; } } catch (Exception exception2) { error = exception2; } // 这里也解释了操作完成时会触发Completed事件 // 分析过程和调用RunWorkerAsync方法触发DoWork事件类似 RunWorkerCompletedEventArgs arg = new RunWorkerCompletedEventArgs(result, error, cancelled); this.asyncOperation.PostOperationCompleted(this.operationCompleted, arg); }
4. 上面的代码中可以知道WorkerThreadStart调用了受保护的OnDoWork方法,下面就让我们看看OnDoWork方法的代码,到这里我们离事物的本质已经不远了。
// OnDoWork的源码 protected virtual void OnDoWork(DoWorkEventArgs e) { // 从事件集合中获得委托对象 DoWorkEventHandler handler = (DoWorkEventHandler) base.Events[doWorkKey]; if (handler != null) { // 调用委托,也就是调用注册DoWork事件的方法 // 我们在使用BackgroundWorker对象的时候,首先需要对它的DoWork事件进行注册
// 到这里就可以解释为什么调用RunWorkerAsync方法会触发DoWork事件了 handler(this, e); } } // 当我们使用+=符号对DoWork事件进行注册时,背后调用确实Add方法,具体可以查看我的事件专题。 public event DoWorkEventHandler DoWork { add { // 把注册的方法名添加进一个事件集合中 // 这个事件集合也是类似一个字典,doWorkKey是注册方法的key,通过这个key就可以获得包装注册方法的委托 base.Events.AddHandler(doWorkKey, value); } remove { base.Events.RemoveHandler(doWorkKey, value); } }
从上面的代码中的注释我们可以解释一开始的疑惑,并且也更好地解释了事件特性,关于事件,你也可以参看我的事件专题(事件也是委托,归根究底又是委托啊,从而可见委委托是多么的重要,同时建议大家在理解委托的时候,可以根据后面的特性重复地去理解)。
对于开始表格中提出的其他的几个疑惑的分析思路和这个分析思路类似,大家可以按照这个思路自己去深入理解下BackgroundWorker类,这里我就不多解释了。相信大家通过上面我的分析可以很快解决其他几个疑惑的,如果你完全理解上面的分析相信你会对EAP,委托和事件又有进一步的理解。
四、使用BackgroundWorker组件进行异步编程
剖析完了BackgroundWorker组件之后,我们是不是很想看看如何使用这个类来实现异步编程呢?下面向大家演示一个使用BackgroundWorker组件实现异步下载文件的一个小程序,该程序支持异步下载(指的就是用线程池线程要执行下载操作),断点续传、下载取消和进度报告的功能,通过这个程序,相信大家也会对基于事件的异步模式有一个更好的理解和知道该模式可以完成一些什么样的任务,下面就看看该程序的主要代码的(因为代码中都有详细的解释,这里就不多解释代码的实现了):
// Begin Start Download file or Resume the download private void btnDownload_Click(object sender, EventArgs e) { if (bgWorkerFileDownload.IsBusy != true) { // Start the asynchronous operation // Fire DoWork Event bgWorkerFileDownload.RunWorkerAsync(); // Create an instance of the RequestState requestState = new RequestState(downloadPath); requestState.filestream.Seek(DownloadSize, SeekOrigin.Begin); this.btnDownload.Enabled = false; this.btnPause.Enabled = true; } else { MessageBox.Show("正在执行操作,请稍后"); } } // Pause Download private void btnPause_Click(object sender, EventArgs e) { if (bgWorkerFileDownload.IsBusy&&bgWorkerFileDownload.WorkerSupportsCancellation == true) { // Pause the asynchronous operation // Fire RunWorkerCompleted event bgWorkerFileDownload.CancelAsync(); } } #region BackGroundWorker Event // Occurs when RunWorkerAsync is called. private void bgWorkerFileDownload_DoWork(object sender, DoWorkEventArgs e) { // Get the source of event BackgroundWorker bgworker = sender as BackgroundWorker; try { // Do the DownLoad operation // Initialize an HttpWebRequest object HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim()); // If the part of the file have been downloaded, // The server should start sending data from the DownloadSize to the end of the data in the HTTP entity. if (DownloadSize != 0) { myHttpWebRequest.AddRange(DownloadSize); } // assign HttpWebRequest instance to its request field. requestState.request = myHttpWebRequest; requestState.response = (HttpWebResponse)myHttpWebRequest.GetResponse(); requestState.streamResponse = requestState.response.GetResponseStream(); int readSize = 0; while (true) { if (bgworker.CancellationPending == true) { e.Cancel = true; break; } readSize = requestState.streamResponse.Read(requestState.BufferRead, 0, requestState.BufferRead.Length); if (readSize > 0) { DownloadSize += readSize; int percentComplete = (int)((float)DownloadSize / (float)totalSize * 100); requestState.filestream.Write(requestState.BufferRead, 0, readSize); // 报告进度,引发ProgressChanged事件的发生 bgworker.ReportProgress(percentComplete); } else { break; } } } catch { throw; } } // Occurs when ReportProgress is called. private void bgWorkerFileDownload_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.progressBar1.Value = e.ProgressPercentage; } // Occurs when the background operation has completed, has been canceled, or has raised an exception. private void bgWorkerFileDownload_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { MessageBox.Show(e.Error.Message); requestState.response.Close(); } else if (e.Cancelled) { MessageBox.Show(String.Format("下载暂停,下载的文件地址为:{0}\n 已经下载的字节数为: {1}字节", downloadPath, DownloadSize)); requestState.response.Close(); requestState.filestream.Close(); this.btnDownload.Enabled = true; this.btnPause.Enabled = false; } else { MessageBox.Show(String.Format("下载已完成,下载的文件地址为:{0},文件的总字节数为: {1}字节", downloadPath, totalSize)); this.btnDownload.Enabled = false; this.btnPause.Enabled = false; requestState.response.Close(); requestState.filestream.Close(); } } #endregion // Get Total Size of File private void GetTotalSize() { HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim()); HttpWebResponse response = (HttpWebResponse)myHttpWebRequest.GetResponse(); totalSize = response.ContentLength; response.Close(); } // This class stores the State of the request. public class RequestState { public int BufferSize = 2048; public byte[] BufferRead; public HttpWebRequest request; public HttpWebResponse response; public Stream streamResponse; public FileStream filestream; public RequestState(string downloadPath) { BufferRead = new byte[BufferSize]; request = null; streamResponse = null; filestream = new FileStream(downloadPath, FileMode.OpenOrCreate); } }
运行程序点击"下载"按钮然后再点击"暂停"后的结果:
当暂停下载后,我们还可以点 ”下载“按钮继续下载该文件,此时并不会从开开始下载,而会接着上次的下载继续下载(这个实现主要是通过AddRange方法来实现的,该方法是指出向服务器请求文件的大小,上面代码中通过传入DownloadSize来告诉服务器,这次我需要的内容不是从开头开始的,而是从已经下载的文件字节数开始到该文件的总的字节结尾,这样就就实现了断点续传的功能了,使户暂停下载不至于之前下载的都白费了。),程序的运行结果为:
五、小结
到这里,本专题的内容就介绍完了,本专题主要介绍.NET 2.0中提出的新的异步编程模式——基于事件的异步编程模式,相信通过本专题的介绍,你将对EAP有一定的了解,并且对BackgroundWorker组件、委托和事件也会有深入地的理解,而不再停留在只会使用该组件阶段,而是到达”会知其然之气所以然“的一个阶段。后面的一个专题将会为大家介绍。NET 4.0中提出的最新的异步编程模式,也是进行异步编程推荐的一种编程模式,即基于任务的编程模式(TAP)。
本专题源码下载地址:https://files.cnblogs.com/zhili/Event-basedAsynchronousPattern.zip