异步编程(二)基于事件的异步编程模式 (EAP)
一、引言
在上一个专题中为大家介绍了.NET 1.0中提出来的异步编程模式——APM,虽然APM为我们实现异步编程提供了一定的支持,同时它也存在着一些明显的问题——不支持对异步操作的取消和没有提供对进度报告的功能,对于有界面的应用程序来说,进度报告和取消操作的支持也是必不可少的,既然存在这样的问题,微软当然也应该提供给我们解决问题的方案了,所以微软在.NET 2.0的时候就为我们提供了一个新的异步编程模型,也就是我这个专题中介绍的基于事件的异步编程模型——EAP。
实现了基于事件的异步模式的类将具有一个或者多个以Async为后缀的方法和对应的Completed事件,并且这些类都支持异步方法的取消、进度报告和报告结果。
当我们调用实现基于事件的异步模式的类的 XxxAsync方法时,即代表开始了一个异步操作,该方法调用完之后会使一个线程池线程去执行耗时的操作,所以当UI线程调用该方法时,当然也就不会堵塞UI线程了。
二、深入剖析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方法就会触发DoWorker事件?
// RunWorkerAsync的源码什么都没有做,只是调用了该方法的重载方法RunWorkerAsync(objectargument)方法 publicvoidRunWorkerAsync() { this.RunWorkerAsync(null); }
// 下面就看看RunWorkerAsync带有一个参数的重载方法的源码 publicvoidRunWorkerAsync(object argument) { if (this.isRunning) { thrownewInvalidOperationException(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); }
1. 我们从上面的代码可以看到调用RunWorkerAsync方法就是调用threadStart委托,我们要知道RunWorkerAsync方法到底背后发生了什么事情,就首先需要知道threadStart委托包装了哪个方法?并且需要知道委托在什么地方实例化的? 2. 委托什么地方实例化话的?谈到实例化当然大家首先想到的就是构造函数了,不错,我们就看看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方法的代码: privatevoidWorkerThreadStart(object argument) { objectresult =null; Exceptionerror =null; boolcancelled =false; try { DoWorkEventArgs e =newDoWorkEventArgs(argument); //该方法中又是调用了onDoWork方法 // this.OnDoWork(e); if(e.Cancel) { cancelled =true; } else { result = e.Result; } } catch(Exception exception2) { error= exception2; } //这里也解释了操作完成时会触发Completed事件 // 分析过程和调用RunWorkerAsync方法触发DoWork事件类似 RunWorkerCompletedEventArgs arg =newRunWorkerCompletedEventArgs(result, error, cancelled); this.asyncOperation.PostOperationCompleted(this.operationCompleted,arg); } 4. 上面的代码中可以知道WorkerThreadStart调用了受保护的OnDoWork方法,下面就让我们看看OnDoWork方法的代码,到这里我们离事物的本质已经不远了。 // OnDoWork的源码 protectedvirtualvoidOnDoWork(DoWorkEventArgs e) { //从事件集合中获得委托对象 DoWorkEventHandler handler = (DoWorkEventHandler)base.Events[doWorkKey]; if(handler !=null) { //调用委托,也就是调用注册DoWork事件的方法 // 我们在使用BackgroundWorker对象的时候,首先需要对它的DoWork事件进行注册 //到这里就可以解释为什么调用RunWorkerAsync方法会触发DoWork事件了 handler(this, e); } } // 当我们使用+=符号对DoWork事件进行注册时,背后调用确实Add方法,具体可以查看我的事件专题。 publiceventDoWorkEventHandler 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 privatevoidbtnDownload_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 =newRequestState(downloadPath); requestState.filestream.Seek(DownloadSize, SeekOrigin.Begin); this.btnDownload.Enabled =false; this.btnPause.Enabled =true; } else { MessageBox.Show("正在执行操作,请稍后"); } } //Pause Download privatevoidbtnPause_Click(object sender, EventArgs e) { if(bgWorkerFileDownload.IsBusy&&bgWorkerFileDownload.WorkerSupportsCancellation==true) { // Pause the asynchronous operation // Fire RunWorkerCompleted event bgWorkerFileDownload.CancelAsync(); } } #regionBackGroundWorker Event //Occurs when RunWorkerAsync is called. privatevoidbgWorkerFileDownload_DoWork(object sender,DoWorkEventArgs e) { // Getthe source of event BackgroundWorker bgworker = senderasBackgroundWorker; 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 endof 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; intpercentComplete = (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. privatevoidbgWorkerFileDownload_ProgressChanged(object sender,ProgressChangedEventArgs e) { this.progressBar1.Value= e.ProgressPercentage; } //Occurs when the background operation has completed, has been canceled, or hasraised an exception. privatevoidbgWorkerFileDownload_RunWorkerCompleted(objectsender, RunWorkerCompletedEventArgs e) { if(e.Error !=null) { MessageBox.Show(e.Error.Message); requestState.response.Close(); } elseif(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 // GetTotal Size of File privatevoidGetTotalSize() { 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. publicclassRequestState { publicintBufferSize =2048; publicbyte[]BufferRead; publicHttpWebRequest request; publicHttpWebResponse response; publicStream streamResponse; publicFileStream filestream; publicRequestState(string downloadPath) { BufferRead =newbyte[BufferSize]; request =null; streamResponse =null; filestream =new FileStream(downloadPath,FileMode.OpenOrCreate); } }