您好,登錄后才能下訂單哦!
本專題概要:
引言
你聽說過EAP嗎?——基于事件異步編程模式介紹
深入剖析BackgroundWorker組件類
使用BackgroundWorker組件進行異步編程
小結
在上一個專題中為大家介紹了.NET 1.0中提出來的異步編程模式——APM,雖然APM為我們實現異步編程提供了一定的支持,同時它也存在著一些明顯的問題——不支持對異步操作的取消和沒有提供對進度報告的功能,對于有界面的應用程序來說,進度報告和取消操作的支持也是必不可少的,既然存在這樣的問題,微軟當然也應該提供給我們解決問題的方案了,所以微軟在.NET 2.0的時候就為我們提供了一個新的異步編程模型,也就是我這個專題中介紹的基于事件的異步編程模型——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.Media.SoundPlay
System.Net.WebClient
System.Net.NetworkInformation.Ping
System.Windows.Forms.PictureBox(繼承于Control類,Control類派生于Component類)
當我們調用實現基于事件的異步模式的類的 XxxAsync方法時,即代表開始了一個異步操作,該方法調用完之后會使一個線程池線程去執行耗時的操作,所以當UI線程調用該方法時,當然也就不會堵塞UI線程了。并且基于事件的異步模式是建立了APM的基礎之上的(這也是我在上一專題中詳解介紹APM的原因),而APM又是建立了在委托之上的(對于這點可以參考該系列的APM專題)。然而這點并不是憑空想象的,下面就BackgroundWorker類來給大家詳解解釋EAP是建立在APM的基礎上的。
在深入講解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.progre***eporter = new SendOrPostCallback(this.Progre***eporter); }
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組件實現異步下載文件的一個小程序,該程序支持異步下載(指的就是用線程池線程要執行下載操作),斷點續傳、下載取消和進度報告的功能,通過這個程序,相信大家也會對基于事件的異步模式有一個更好的理解和知道該模式可以完成一些什么樣的任務,下面就看看該程序的主要代碼的(因為代碼中都有詳細的解釋,這里就不多解釋代碼的實現了):
// 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)。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。