weidagang2046的專欄

          物格而后知致
          隨筆 - 8, 文章 - 409, 評論 - 101, 引用 - 0
          數(shù)據(jù)加載中……

          通過多線程為基于.NET 的應(yīng)用程序?qū)崿F(xiàn)響應(yīng)迅速的用戶

          用戶不喜歡反應(yīng)慢的程序。程序反應(yīng)越慢,就越?jīng)]有用戶會喜歡它。在執(zhí)行耗時較長的操作時,使用多線程是明智之舉,它可以提高程序 UI 的響應(yīng)速度,使得一切運(yùn)行顯得更為快速。在 Windows 中進(jìn)行多線程編程曾經(jīng)是 C++ 開發(fā)人員的專屬特權(quán),但是現(xiàn)在,可以使用所有兼容 Microsoft .NET 的語言來編寫,其中包括 Visual Basic.NET。不過,Windows 窗體對線程的使用強(qiáng)加了一些重要限制。本文將對這些限制進(jìn)行闡釋,并說明如何利用它們來提供快速、高質(zhì)量的 UI 體驗(yàn),即使是程序要執(zhí)行的任務(wù)本身速度就較慢。

          為什么選擇多線程?


          多線程程序要比單線程程序更難于編寫,并且不加選擇地使用線程也是導(dǎo)致難以找到細(xì)小錯誤的重要原因。這就自然會引出兩個問題:為什么不堅(jiān)持編寫單線程代碼?如果必須使用多線程,如何才能避免缺陷呢?本文的大部分篇幅都是在回答第二個問題,但首先我要來解釋一下為什么確實(shí)需要多線程。

          多線程處理可以使您能夠通過確保程序“永不睡眠”從而保持 UI 的快速響應(yīng)。大部分程序都有不響應(yīng)用戶的時候:它們正忙于為您執(zhí)行某些操作以便響應(yīng)進(jìn)一步的請求。也許最廣為人知的例子就是出現(xiàn)在“打開文件”對話框頂部的組合框。如果在展開該組合框時,CD-ROM驅(qū)動器里恰好有一張光盤,則計算機(jī)通常會在顯示列表之前先讀取光盤。這可能需要幾秒鐘的時間,在此期間,程序既不響應(yīng)任何輸入,也不允許取消該操作,尤其是在確實(shí)并不打算使用光驅(qū)的時候,這種情況會讓人無法忍受。

          執(zhí)行這種操作期間 UI 凍結(jié)的原因在于,UI 是個單線程程序,單線程不可能在等待 CD-ROM驅(qū)動器讀取操作的同時處理用戶輸入,如圖 1 所示。“打開文件”對話框會調(diào)用某些阻塞 (blocking) API 來確定 CD-ROM 的標(biāo)題。阻塞 API 在未完成自己的工作之前不會返回,因此這期間它會阻止線程做其他事情。


          圖 1 單線程


          在多線程下,像這樣耗時較長的任務(wù)就可以在其自己的線程中運(yùn)行,這些線程通常稱為輔助線程。因?yàn)橹挥休o助線程受到阻止,所以阻塞操作不再導(dǎo)致用戶界面凍結(jié),如圖 2 所示。應(yīng)用程序的主線程可以繼續(xù)處理用戶的鼠標(biāo)和鍵盤輸入的同時,受阻的另一個線程將等待 CD-ROM 讀取,或執(zhí)行輔助線程可能做的任何操作。


          圖 2 多線程


          其基本原則是,負(fù)責(zé)響應(yīng)用戶輸入和保持用戶界面為最新的線程(通常稱為 UI 線程)不應(yīng)該用于執(zhí)行任何耗時較長的操作。慣常做法是,任何耗時超過 30ms 的操作都要考慮從 UI 線程中移除。這似乎有些夸張,因?yàn)?30ms 對于大多數(shù)人而言只不過是他們可以感覺到的最短的瞬間停頓,實(shí)際上該停頓略短于電影屏幕中顯示的連續(xù)幀之間的間隔。

          如果鼠標(biāo)單擊和相應(yīng)的 UI 提示(例如,重新繪制按鈕)之間的延遲超過 30ms,那么操作與顯示之間就會稍顯不連貫,并因此產(chǎn)生如同影片斷幀那樣令人心煩的感覺。為了達(dá)到完全高質(zhì)量的響應(yīng)效果,上限必須是 30ms。另一方面,如果您確實(shí)不介意感覺稍顯不連貫,但也不想因?yàn)橥nD過長而激怒用戶,則可按照通常用戶所能容忍的限度將該間隔設(shè)為 100ms。

          這意味著如果想讓用戶界面保持響應(yīng)迅速,則任何阻塞操作都應(yīng)該在輔助線程中執(zhí)行 — 不管是機(jī)械等待某事發(fā)生(例如,等待 CD-ROM 啟動或者硬盤定位數(shù)據(jù)),還是等待來自網(wǎng)絡(luò)的響應(yīng)。

          異步委托調(diào)用


          在輔助線程中運(yùn)行代碼的最簡單方式是使用異步委托調(diào)用(所有委托都提供該功能)。委托通常是以同步方式進(jìn)行調(diào)用,即,在調(diào)用委托時,只有包裝方法返回后該調(diào)用才會返回。要以異步方式調(diào)用委托,請調(diào)用 BeginInvoke 方法,這樣會對該方法排隊(duì)以在系統(tǒng)線程池的線程中運(yùn)行。調(diào)用線程會立即返回,而不用等待該方法完成。這比較適合于 UI 程序,因?yàn)榭梢杂盟鼇韱雍臅r較長的作業(yè),而不會使用戶界面反應(yīng)變慢。

          例如,在以下代碼中,System.Windows.Forms.MethodInvoker 類型是一個系統(tǒng)定義的委托,用于調(diào)用不帶參數(shù)的方法。

          private void StartSomeWorkFromUIThread () {
              // The work we want to do is too slow for the UI
              // thread, so let's farm it out to a worker thread.
          
              MethodInvoker mi = new MethodInvoker(
                  RunsOnWorkerThread);
              mi.BeginInvoke(null, null); // This will not block.
          }
          
          // The slow work is done here, on a thread
          // from the system thread pool.
          private void RunsOnWorkerThread() {
              DoSomethingSlow();
          }
          

          如果想要傳遞參數(shù),可以選擇合適的系統(tǒng)定義的委托類型,或者自己來定義委托。MethodInvoker 委托并沒有什么神奇之處。和其他委托一樣,調(diào)用 BeginInvoke 會使該方法在系統(tǒng)線程池的線程中運(yùn)行,而不會阻塞 UI 線程以便其可執(zhí)行其他操作。對于以上情況,該方法不返回數(shù)據(jù),所以啟動它后就不用再去管它。如果您需要該方法返回的結(jié)果,則 BeginInvoke 的返回值很重要,并且您可能不傳遞空參數(shù)。然而,對于大多數(shù) UI 應(yīng)用程序而言,這種“啟動后就不管”的風(fēng)格是最有效的,稍后會對原因進(jìn)行簡要討論。您應(yīng)該注意到,BeginInvoke 將返回一個 IAsyncResult。這可以和委托的 EndInvoke 方法一起使用,以在該方法調(diào)用完畢后檢索調(diào)用結(jié)果。

          還有其他一些可用于在另外的線程上運(yùn)行方法的技術(shù),例如,直接使用線程池 API 或者創(chuàng)建自己的線程。然而,對于大多數(shù)用戶界面應(yīng)用程序而言,有異步委托調(diào)用就足夠了。采用這種技術(shù)不僅編碼容易,而且還可以避免創(chuàng)建并非必需的線程,因?yàn)榭梢岳镁€程池中的共享線程來提高應(yīng)用程序的整體性能。

          線程和控件


          Windows 窗體體系結(jié)構(gòu)對線程使用制定了嚴(yán)格的規(guī)則。如果只是編寫單線程應(yīng)用程序,則沒必要知道這些規(guī)則,這是因?yàn)閱尉€程的代碼不可能違反這些規(guī)則。然而,一旦采用多線程,就需要理解 Windows 窗體中最重要的一條線程規(guī)則:除了極少數(shù)的例外情況,否則都不要在它的創(chuàng)建線程以外的線程中使用控件的任何成員。

          本規(guī)則的例外情況有文檔說明,但這樣的情況非常少。這適用于其類派生自 System.Windows.Forms.Control 的任何對象,其中幾乎包括 UI 中的所有元素。所有的 UI 元素(包括表單本身)都是從 Control 類派生的對象。此外,這條規(guī)則的結(jié)果是一個被包含的控件(如,包含在一個表單中的按鈕)必須與包含它控件位處于同一個線程中。也就是說,一個窗口中的所有控件屬于同一個 UI 線程。實(shí)際中,大部分 Windows 窗體應(yīng)用程序最終都只有一個線程,所有 UI 活動都發(fā)生在這個線程上。這個線程通常稱為 UI 線程。這意味著您不能調(diào)用用戶界面中任意控件上的任何方法,除非在該方法的文檔說明中指出可以調(diào)用。該規(guī)則的例外情況(總有文檔記錄)非常少而且它們之間關(guān)系也不大。請注意,以下代碼是非法的:

          // Created on UI thread
          private Label lblStatus;
          ...
          // Doesn't run on UI thread
          private void RunsOnWorkerThread() {
              DoSomethingSlow();
              lblStatus.Text = "Finished!";    // BAD!!
          }
          

          如果您在 .NET Framework 1.0 版本中嘗試運(yùn)行這段代碼,也許會僥幸運(yùn)行成功,或者初看起來是如此。這就是多線程錯誤中的主要問題,即它們并不會立即顯現(xiàn)出來。甚至當(dāng)出現(xiàn)了一些錯誤時,在第一次演示程序之前一切看起來也都很正常。但不要搞錯 — 我剛才顯示的這段代碼明顯違反了規(guī)則,并且可以預(yù)見,任何抱希望于“試運(yùn)行時良好,應(yīng)該就沒有問題”的人在即將到來的調(diào)試期是會付出沉重代價的。

          要注意,在明確創(chuàng)建線程之前會發(fā)生這樣的問題。使用委托的異步調(diào)用實(shí)用程序(調(diào)用它的 BeginInvoke 方法)的任何代碼都可能出現(xiàn)同樣的問題。委托提供了一個非常吸引人的解決方案來處理 UI 應(yīng)用程序中緩慢、阻塞的操作,因?yàn)檫@些委托能使您輕松地讓此種操作運(yùn)行在 UI 線程外而無需自己創(chuàng)建新線程。但是由于以異步委托調(diào)用方式運(yùn)行的代碼在一個來自線程池的線程中運(yùn)行,所以它不能訪問任何 UI 元素。上述限制也適用于線程池中的線程和手動創(chuàng)建的輔助線程。

          在正確的線程中調(diào)用控件


          有關(guān)控件的限制看起來似乎對多線程編程非常不利。如果在輔助線程中運(yùn)行的某個緩慢操作不對 UI 產(chǎn)生任何影響,用戶如何知道它的進(jìn)行情況呢?至少,用戶如何知道工作何時完成或者是否出現(xiàn)錯誤?幸運(yùn)的是,雖然此限制的存在會造成不便,但并非不可逾越。有多種方式可以從輔助線程獲取消息,并將該消息傳遞給 UI 線程。理論上講,可以使用低級的同步原理和池化技術(shù)來生成自己的機(jī)制,但幸運(yùn)的是,因?yàn)橛幸粋€以 Control 類的 Invoke 方法形式存在的解決方案,所以不需要借助于如此低級的工作方式。

          Invoke 方法是 Control 類中少數(shù)幾個有文檔記錄的線程規(guī)則例外之一:它始終可以對來自任何線程的 Control 進(jìn)行 Invoke 調(diào)用。Invoke 方法本身只是簡單地攜帶委托以及可選的參數(shù)列表,并在 UI 線程中為您調(diào)用委托,而不考慮 Invoke 調(diào)用是由哪個線程發(fā)出的。實(shí)際上,為控件獲取任何方法以在正確的線程上運(yùn)行非常簡單。但應(yīng)該注意,只有在 UI 線程當(dāng)前未受到阻塞時,這種機(jī)制才有效 — 調(diào)用只有在 UI 線程準(zhǔn)備處理用戶輸入時才能通過。從不阻塞 UI 線程還有另一個好理由。Invoke 方法會進(jìn)行測試以了解調(diào)用線程是否就是 UI 線程。如果是,它就直接調(diào)用委托。否則,它將安排線程切換,并在 UI 線程上調(diào)用委托。無論是哪種情況,委托所包裝的方法都會在 UI 線程中運(yùn)行,并且只有當(dāng)該方法完成時,Invoke 才會返回。

          Control 類也支持異步版本的 Invoke,它會立即返回并安排該方法以便在將來某一時間在 UI 線程上運(yùn)行。這稱為 BeginInvoke,它與異步委托調(diào)用很相似,與委托的明顯區(qū)別在于,該調(diào)用以異步方式在線程池的某個線程上運(yùn)行,然而在此處,它以異步方式在 UI 線程上運(yùn)行。實(shí)際上,Control 的 Invoke、BeginInvoke 和 EndInvoke 方法,以及 InvokeRequired 屬性都是 ISynchronizeInvoke 接口的成員。該接口可由任何需要控制其事件傳遞方式的類實(shí)現(xiàn)。

          由于 BeginInvoke 不容易造成死鎖,所以盡可能多用該方法;而少用 Invoke 方法。因?yàn)?Invoke 是同步的,所以它會阻塞輔助線程,直到 UI 線程可用。但是如果 UI 線程正在等待輔助線程執(zhí)行某操作,情況會怎樣呢?應(yīng)用程序會死鎖。BeginInvoke 從不等待 UI 線程,因而可以避免這種情況。

          現(xiàn)在,我要回顧一下前面所展示的代碼片段的合法版本。首先,必須將一個委托傳遞給 Control 的 BeginInvoke 方法,以便可以在 UI 線程中運(yùn)行對線程敏感的代碼。這意味著應(yīng)該將該代碼放在它自己的方法中,如圖 3 所示。一旦輔助線程完成緩慢的工作后,它就會調(diào)用 Label 中的 BeginInvoke,以便在其 UI 線程上運(yùn)行某段代碼。通過這樣,它可以更新用戶界面。

          包裝 Control.Invoke


          雖然圖 3中的代碼解決了這個問題,但它相當(dāng)繁瑣。如果輔助線程希望在結(jié)束時提供更多的反饋信息,而不是簡單地給出“Finished!”消息,則 BeginInvoke 過于復(fù)雜的使用方法會令人生畏。為了傳達(dá)其他消息,例如“正在處理”、“一切順利”等等,需要設(shè)法向 UpdateUI 函數(shù)傳遞一個參數(shù)。可能還需要添加一個進(jìn)度欄以提高反饋能力。這么多次調(diào)用 BeginInvoke 可能導(dǎo)致輔助線程受該代碼支配。這樣不僅會造成不便,而且考慮到輔助線程與 UI 的協(xié)調(diào)性,這樣設(shè)計也不好。對這些進(jìn)行分析之后,我們認(rèn)為包裝函數(shù)可以解決這兩個問題,如圖 4 所示。

          ShowProgress 方法對將調(diào)用引向正確線程的工作進(jìn)行封裝。這意味著輔助線程代碼不再擔(dān)心需要過多關(guān)注 UI 細(xì)節(jié),而只要定期調(diào)用 ShowProgress 即可。請注意,我定義了自己的方法,該方法違背了“必須在 UI 線程上進(jìn)行調(diào)用”這一規(guī)則,因?yàn)樗M(jìn)而只調(diào)用不受該規(guī)則約束的其他方法。這種技術(shù)會引出一個較為常見的話題:為什么不在控件上編寫公共方法呢(這些方法記錄為 UI 線程規(guī)則的例外)?

          剛好 Control 類為這樣的方法提供了一個有用的工具。如果我提供一個設(shè)計為可從任何線程調(diào)用的公共方法,則完全有可能某人會從 UI 線程調(diào)用這個方法。在這種情況下,沒必要調(diào)用 BeginInvoke,因?yàn)槲乙呀?jīng)處于正確的線程中。調(diào)用 Invoke 完全是浪費(fèi)時間和資源,不如直接調(diào)用適當(dāng)?shù)姆椒ā榱吮苊膺@種情況,Control 類將公開一個稱為 InvokeRequired 的屬性。這是“只限 UI 線程”規(guī)則的另一個例外。它可從任何線程讀取,如果調(diào)用線程是 UI 線程,則返回假,其他線程則返回真。這意味著我可以按以下方式修改包裝:

          public void ShowProgress(string msg, int percentDone) {
              if (InvokeRequired) {
                  // As before
                  ...
              } else {
                  // We're already on the UI thread just
                  // call straight through.
                  UpdateUI(this, new MyProgressEvents(msg,
                      PercentDone));
              }
          }
          

          ShowProgress 現(xiàn)在可以記錄為可從任何線程調(diào)用的公共方法。這并沒有消除復(fù)雜性 — 執(zhí)行 BeginInvoke 的代碼依然存在,它還占有一席之地。不幸的是,沒有簡單的方法可以完全擺脫它。

          鎖定


          任何并發(fā)系統(tǒng)都必須面對這樣的事實(shí),即,兩個線程可能同時試圖使用同一塊數(shù)據(jù)。有時這并不是問題 — 如果多個線程在同一時間試圖讀取某個對象中的某個字段,則不會有問題。然而,如果有線程想要修改該數(shù)據(jù),就會出現(xiàn)問題。如果線程在讀取時剛好另一個線程正在寫入,則讀取線程有可能會看到虛假值。如果兩個線程在同一時間、在同一個位置執(zhí)行寫入操作,則在同步寫入操作發(fā)生之后,所有從該位置讀取數(shù)據(jù)的線程就有可能看到一堆垃圾數(shù)據(jù)。雖然這種行為只在特定情況下才會發(fā)生,讀取操作甚至不會與寫入操作發(fā)生沖突,但是數(shù)據(jù)可以是兩次寫入結(jié)果的混加,并保持錯誤結(jié)果直到下一次寫入值為止。為了避免這種問題,必須采取措施來確保一次只有一個線程可以讀取或?qū)懭肽硞€對象的狀態(tài)。

          防止這些問題出現(xiàn)所采用的方式是,使用運(yùn)行時的鎖定功能。C# 可以讓您利用這些功能、通過鎖定關(guān)鍵字來保護(hù)代碼(Visual Basic 也有類似構(gòu)造,稱為 SyncLock)。規(guī)則是,任何想要在多個線程中調(diào)用其方法的對象在每次訪問其字段時(不管是讀取還是寫入)都應(yīng)該使用鎖定構(gòu)造。例如,請參見圖 5

          鎖定構(gòu)造的工作方式是:公共語言運(yùn)行庫 (CLR) 中的每個對象都有一個與之相關(guān)的鎖,任何線程均可獲得該鎖,但每次只能有一個線程擁有它。如果某個線程試圖獲取另一個線程已經(jīng)擁有的鎖,那么它必須等待,直到擁有該鎖的線程將鎖釋放為止。C# 鎖定構(gòu)造會獲取該對象鎖(如果需要,要先等待另一個線程利用它完成操作),并保留到大括號中的代碼退出為止。如果執(zhí)行語句運(yùn)行到塊結(jié)尾,該鎖就會被釋放,并從塊中部返回,或者拋出在塊中沒有捕捉到的異常。

          請注意,MoveBy 方法中的邏輯受同樣的鎖語句保護(hù)。當(dāng)所做的修改比簡單的讀取或?qū)懭敫鼜?fù)雜時,整個過程必須由單獨(dú)的鎖語句保護(hù)。這也適用于對多個字段進(jìn)行更新 — 在對象處于一致狀態(tài)之前,一定不能釋放該鎖。如果該鎖在更新狀態(tài)的過程中釋放,則其他線程也許能夠獲得它并看到不一致狀態(tài)。如果您已經(jīng)擁有一個鎖,并調(diào)用一個試圖獲取該鎖的方法,則不會導(dǎo)致問題出現(xiàn),因?yàn)閱为?dú)線程允許多次獲得同一個鎖。對于需要鎖定以保護(hù)對字段的低級訪問和對字段執(zhí)行的高級操作的代碼,這非常重要。MoveBy 使用 Position 屬性,它們同時獲得該鎖。只有最外面的鎖阻塞完成后,該鎖才會恰當(dāng)?shù)蒯尫拧?/p>

          對于需要鎖定的代碼,必須嚴(yán)格進(jìn)行鎖定。稍有疏漏,便會功虧一簣。如果一個方法在沒有獲取對象鎖的情況下修改狀態(tài),則其余的代碼在使用它之前即使小心地鎖定對象也是徒勞。同樣,如果一個線程在沒有事先獲得鎖的情況下試圖讀取狀態(tài),則它可能讀取到不正確的值。運(yùn)行時無法進(jìn)行檢查來確保多線程代碼正常運(yùn)行。

          死鎖


          鎖是確保多線程代碼正常運(yùn)行的基本條件,即使它們本身也會引入新的風(fēng)險。在另一個線程上運(yùn)行代碼的最簡單方式是,使用異步委托調(diào)用(請參見圖 6)。

          如果曾經(jīng)調(diào)用過 Foo 的 CallBar 方法,則這段代碼會慢慢停止運(yùn)行。CallBar 方法將獲得 Foo 對象上的鎖,并直到 BarWork 返回后才釋放它。然后,BarWork 使用異步委托調(diào)用,在某個線程池線程中調(diào)用 Foo 對象的 FooWork 方法。接下來,它會在調(diào)用委托的 EndInvoke 方法前執(zhí)行一些其他操作。EndInvoke 將等待輔助線程完成,但輔助線程卻被阻塞在 FooWork 中。它也試圖獲取 Foo 對象的鎖,但鎖已被 CallBar 方法持有。所以,F(xiàn)ooWork 會等待 CallBar 釋放鎖,但 CallBar 也在等待 BarWork 返回。不幸的是,BarWork 將等待 FooWork 完成,所以 FooWork 必須先完成,它才能開始。結(jié)果,沒有線程能夠進(jìn)行下去。

          這就是一個死鎖的例子,其中有兩個或更多線程都被阻塞以等待對方進(jìn)行。這里的情形和標(biāo)準(zhǔn)死鎖情況還是有些不同,后者通常包括兩個鎖。這表明如果有某個因果性(過程調(diào)用鏈)超出線程界限,就會發(fā)生死鎖,即使只包括一個鎖!Control.Invoke 是一種跨線程調(diào)用過程的方法,這是個不爭的重要事實(shí)。BeginInvoke 不會遇到這樣的問題,因?yàn)樗⒉粫挂蚬钥缇€程。實(shí)際上,它會在某個線程池線程中啟動一個全新的因果性,以允許原有的那個獨(dú)立進(jìn)行。然而,如果保留 BeginInvoke 返回的 IAsyncResult,并用它調(diào)用 EndInvoke,則又會出現(xiàn)問題,因?yàn)?EndInvoke 實(shí)際上已將兩個因果性合二為一。避免這種情況的最簡單方法是,當(dāng)持有一個對象鎖時,不要等待跨線程調(diào)用完成。要確保這一點(diǎn),應(yīng)該避免在鎖語句中調(diào)用 Invoke 或 EndInvoke。其結(jié)果是,當(dāng)持有一個對象鎖時,將無需等待其他線程完成某操作。要堅(jiān)持這個規(guī)則,說起來容易做起來難。

          在檢查代碼的 BarWork 時,它是否在鎖語句的作用域內(nèi)并不明顯,因?yàn)樵谠摲椒ㄖ胁]有鎖語句。出現(xiàn)這個問題的唯一原因是 BarWork 調(diào)用自 Foo.CallBar 方法的鎖語句。這意味著只有確保正在調(diào)用的函數(shù)并不擁有鎖時,調(diào)用 Control.Invoke 或 EndIn-voke 才是安全的。對于非私有方法而言,確保這一點(diǎn)并不容易,所以最佳規(guī)則是,根本不調(diào)用 Control.Invoke 和 EndInvoke。這就是為什么“啟動后就不管”的編程風(fēng)格更可取的原因,也是為什么 Control.BeginInvoke 解決方案通常比 Control.Invoke 解決方案好的原因。

          有時候除了破壞規(guī)則別無選擇,這種情況下就需要仔細(xì)嚴(yán)格地分析。但只要可能,在持有鎖時就應(yīng)該避免阻塞,因?yàn)槿绻贿@樣,死鎖就難以消除。

          使其簡單


          如何既從多線程獲益最大,又不會遇到困擾并發(fā)代碼的棘手錯誤呢?如果提高的 UI 響應(yīng)速度僅僅是使程序時常崩潰,那么很難說是改善了用戶體驗(yàn)。大部分在多線程代碼中普遍存在的問題都是由要一次運(yùn)行多個操作的固有復(fù)雜性導(dǎo)致的,這是因?yàn)榇蠖鄶?shù)人更善于思考連續(xù)過程而非并發(fā)過程。通常,最好的解決方案是使事情盡可能簡單。

          UI 代碼的性質(zhì)是:它從外部資源接收事件,如用戶輸入。它會在事件發(fā)生時對其進(jìn)行處理,但卻將大部分時間花在了等待事件的發(fā)生。如果可以構(gòu)造輔助線程和 UI 線程之間的通信,使其適合該模型,則未必會遇到這么多問題,因?yàn)椴粫儆行碌臇|西引入。我是這樣使事情簡單化的:將輔助線程視為另一個異步事件源。如同 Button 控件傳遞諸如 Click 和 MouseEnter 這樣的事件,可以將輔助線程視為傳遞事件(如 ProgressUpdate 和 WorkComplete)的某物。只是簡單地將這看作一種類比,還是真正將輔助對象封裝在一個類中,并按這種方式公開適當(dāng)?shù)氖录@完全取決于您。后一種選擇可能需要更多的代碼,但會使用戶界面代碼看起來更加統(tǒng)一。不管哪種情況,都需要 Control.BeginInvoke 在正確的線程上傳遞這些事件。

          對于輔助線程,最簡單的方式是將代碼編寫為正常順序的代碼塊。但如果想要使用剛才介紹的“將輔助線程作為事件源”模型,那又該如何呢?這個模型非常適用,但它對該代碼與用戶界面的交互提出了限制:這個線程只能向 UI 發(fā)送消息,并不能向它提出請求。

          例如,讓輔助線程中途發(fā)起對話以請求完成結(jié)果需要的信息將非常困難。如果確實(shí)需要這樣做,也最好是在輔助線程中發(fā)起這樣的對話,而不要在主 UI 線程中發(fā)起。該約束是有利的,因?yàn)樗鼘⒋_保有一個非常簡單且適用于兩線程間通信的模型 — 在這里簡單是成功的關(guān)鍵。這種開發(fā)風(fēng)格的優(yōu)勢在于,在等待另一個線程時,不會出現(xiàn)線程阻塞。這是避免死鎖的有效策略。

          圖 7 顯示了使用異步委托調(diào)用以在輔助線程中執(zhí)行可能較慢的操作(讀取某個目錄的內(nèi)容),然后將結(jié)果顯示在 UI 上。它還不至于使用高級事件語法,但是該調(diào)用確實(shí)是以與處理事件(如單擊)非常相似的方式來處理完整的輔助代碼。

          取消


          前面示例所帶來的問題是,要取消操作只能通過退出整個應(yīng)用程序?qū)崿F(xiàn)。雖然在讀取某個目錄時 UI 仍然保持迅速響應(yīng),但由于在當(dāng)前操作完成之前程序?qū)⒔孟嚓P(guān)按鈕,所以用戶無法查看另一個目錄。如果試圖讀取的目錄是在一臺剛好沒有響應(yīng)的遠(yuǎn)程機(jī)器上,這就很不幸,因?yàn)檫@樣的操作需要很長時間才會超時。

          要取消一個操作也比較困難,盡管這取決于怎樣才算取消。一種可能的理解是“停止等待這個操作完成,并繼續(xù)另一個操作。”這實(shí)際上是拋棄進(jìn)行中的操作,并忽略最終完成時可能產(chǎn)生的后果。對于當(dāng)前示例,這是最好的選擇,因?yàn)楫?dāng)前正在處理的操作(讀取目錄內(nèi)容)是通過調(diào)用一個阻塞 API 來執(zhí)行的,取消它沒有關(guān)系。但即使是如此松散的“假取消”也需要進(jìn)行大量工作。如果決定啟動新的讀取操作而不等待原來的操作完成,則無法知道下一個接收到的通知是來自這兩個未處理請求中的哪一個。

          支持取消在輔助線程中運(yùn)行的請求的唯一方式是,提供與每個請求相關(guān)的某種調(diào)用對象。最簡單的做法是將它作為一個 Cookie,由輔助線程在每次通知時傳遞,允許 UI 線程將事件與請求相關(guān)聯(lián)。通過簡單的身份比較(參見圖 8),UI 代碼就可以知道事件是來自當(dāng)前請求,還是來自早已廢棄的請求。

          如果簡單拋棄就行,那固然很好,不過您可能想要做得更好。如果輔助線程執(zhí)行的是進(jìn)行一連串阻塞操作的復(fù)雜操作,那么您可能希望輔助線程在最早的時機(jī)停止。否則,它可能會繼續(xù)幾分鐘的無用操作。在這種情況下,調(diào)用對象需要做的就不止是作為一個被動 Cookie。它至少還需要維護(hù)一個標(biāo)記,指明請求是否被取消。UI 可以隨時設(shè)置這個標(biāo)記,而輔助線程在執(zhí)行時將定期測試這個標(biāo)記,以確定是否需要放棄當(dāng)前工作。

          對于這個方案,還需要做出幾個決定:如果 UI 取消了操作,它是否要等待直到輔助線程注意到這次取消?如果不等待,就需要考慮一個爭用條件:有可能 UI 線程會取消該操作,但在設(shè)置控制標(biāo)記之前輔助線程已經(jīng)決定傳遞通知了。因?yàn)?UI 線程決定不等待,直到輔助線程處理取消,所以 UI 線程有可能會繼續(xù)從輔助線程接收通知。如果輔助線程使用 BeginInvoke 異步傳遞通知,則 UI 甚至有可能收到多個通知。UI 線程也可以始終按與“廢棄”做法相同的方式處理通知 — 檢查調(diào)用對象的標(biāo)識并忽略它不再關(guān)心的操作通知。或者,在調(diào)用對象中進(jìn)行鎖定并決不從輔助線程調(diào)用 BeginInvoke 以解決問題。但由于讓 UI 線程在處理一個事件之前簡單地對其進(jìn)行檢查以確定是否有用也比較簡單,所以使用該方法碰到的問題可能會更少。

          請查看“代碼下載”(本文頂部的鏈接)中的 AsyncUtils,它是一個有用的基類,可為基于輔助線程的操作提供取消功能。圖 9 顯示了一個派生類,它實(shí)現(xiàn)了支持取消的遞歸目錄搜索。這些類闡明了一些有趣的技術(shù)。它們都使用 C# 事件語法來提供通知。該基類將公開一些在操作成功完成、取消和拋出異常時出現(xiàn)的事件。派生類對此進(jìn)行了擴(kuò)充,它們將公開通知客戶端搜索匹配、進(jìn)度以及顯示當(dāng)前正在搜索哪個目錄的事件。這些事件始終在 UI 線程中傳遞。實(shí)際上,這些類并未限制為 Control 類 — 它們可以將事件傳遞給實(shí)現(xiàn) ISynchronizeInvoke 接口的任何類。圖 10 是一個示例 Windows 窗體應(yīng)用程序,它為 Search 類提供一個用戶界面。它允許取消搜索并顯示進(jìn)度和結(jié)果。

          程序關(guān)閉


          某些情況下,可以采用“啟動后就不管”的異步操作,而不需要其他復(fù)雜要求來使操作可取消。然而,即使用戶界面不要求取消,有可能還是需要實(shí)現(xiàn)這項(xiàng)功能以使程序可以徹底關(guān)閉。

          當(dāng)應(yīng)用程序退出時,如果由線程池創(chuàng)建的輔助線程還在運(yùn)行,則這些線程會被終止。終止是簡單粗暴的操作,因?yàn)殛P(guān)閉甚至?xí)@開任何還起作用的 Finally 塊。如果異步操作執(zhí)行的某些工作不應(yīng)該以這種方式被打斷,則必須確保在關(guān)閉之前這樣的操作已經(jīng)完成。此類操作可能包括對文件執(zhí)行的寫入操作,但由于突然中斷后,文件可能被破壞。

          一種解決辦法是創(chuàng)建自己的線程,而不用來自輔助線程池的線程,這樣就自然會避開使用異步委托調(diào)用。這樣,即使主線程關(guān)閉,應(yīng)用程序也會等到您的線程退出后才終止。System.Threading.Thread 類有一個 IsBackground 屬性可以控制這種行為。它默認(rèn)為 false,這種情況下,CLR 會等到所有非背景線程都退出后才正常終止應(yīng)用程序。然而,這會帶來另一個問題,因?yàn)閼?yīng)用程序掛起時間可能會比您預(yù)期的長。窗口都關(guān)閉了,但進(jìn)程仍在運(yùn)行。這也許不是個問題。如果應(yīng)用程序只是因?yàn)橐M(jìn)行一些清理工作才比正常情況掛起更長時間,那沒問題。另一方面,如果應(yīng)用程序在用戶界面關(guān)閉后還掛起幾分鐘甚至幾小時,那就不可接受了。例如,如果它仍然保持某些文件打開,則可能妨礙用戶稍后重啟該應(yīng)用程序。

          最佳方法是,如果可能,通常應(yīng)該編寫自己的異步操作以便可以將其迅速取消,并在關(guān)閉應(yīng)用程序之前等待所有未完成的操作完成。這意味著您可以繼續(xù)使用異步委托,同時又能確保關(guān)閉操作徹底且及時。

          錯誤處理


          在輔助線程中出現(xiàn)的錯誤一般可以通過觸發(fā) UI 線程中的事件來處理,這樣錯誤處理方式就和完成及進(jìn)程更新方式完全一樣。因?yàn)楹茈y在輔助線程上進(jìn)行錯誤恢復(fù),所以最簡單的策略就是讓所有錯誤都為致命錯誤。錯誤恢復(fù)的最佳策略是使操作完全失敗,并在 UI 線程上執(zhí)行重試邏輯。如果需要用戶干涉來修復(fù)造成錯誤的問題,簡單的做法是給出恰當(dāng)?shù)奶崾尽?/p>

          AsyncUtils 類處理錯誤以及取消。如果操作拋出異常,該基類就會捕捉到,并通過 Failed 事件將異常傳遞給 UI。

          小結(jié)


          謹(jǐn)慎地使用多線程代碼可以使 UI 在執(zhí)行耗時較長的任務(wù)時不會停止響應(yīng),從而顯著提高應(yīng)用程序的反應(yīng)速度。異步委托調(diào)用是將執(zhí)行速度緩慢的代碼從 UI 線程遷移出來,從而避免此類間歇性無響應(yīng)的最簡單方式。

          Windows Forms Control 體系結(jié)構(gòu)基本上是單線程,但它提供了實(shí)用程序以將來自輔助線程的調(diào)用封送返回至 UI 線程。處理來自輔助線程的通知(不管是成功、失敗還是正在進(jìn)行的指示)的最簡單策略是,以對待來自常規(guī)控件的事件(如鼠標(biāo)單擊或鍵盤輸入)的方式對待它們。這樣可以避免在 UI 代碼中引入新的問題,同時通信的單向性也不容易導(dǎo)致出現(xiàn)死鎖。

          有時需要讓 UI 向一個正在處理的操作發(fā)送消息。其中最常見的是取消一個操作。通過建立一個表示正在進(jìn)行的調(diào)用的對象并維護(hù)由輔助線程定期檢查的取消標(biāo)記可實(shí)現(xiàn)這一目的。如果用戶界面線程需要等待取消被認(rèn)可(因?yàn)橛脩粜枰拦ぷ饕汛_實(shí)終止,或者要求徹底退出程序),實(shí)現(xiàn)起來會有些復(fù)雜,但所提供的示例代碼中包含了一個將所有復(fù)雜性封裝在內(nèi)的基類。派生類只需要執(zhí)行一些必要的工作、周期性測試取消,以及要是因?yàn)槿∠埱蠖V构ぷ鳎蛯⒔Y(jié)果通知基類。

          from: http://dotnet.csdn.net/n/20060726/92986.html

          posted on 2007-01-07 13:46 weidagang2046 閱讀(502) 評論(0)  編輯  收藏 所屬分類: Windows

          主站蜘蛛池模板: 聂拉木县| 浮山县| 旬邑县| 于田县| 宾阳县| 彭阳县| 四平市| 安庆市| 水城县| 土默特左旗| 岳西县| 玉环县| 宿州市| 司法| 古浪县| 商洛市| 县级市| 抚松县| 博乐市| 当雄县| 温宿县| 上犹县| 霸州市| 黑水县| 武平县| 阿合奇县| 巴东县| 卢龙县| 新建县| 申扎县| 襄垣县| 沾化县| 巩留县| 杨浦区| 高唐县| 林州市| 韶关市| 松滋市| 邛崃市| 永兴县| 渝中区|