熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> .NET編程 >> 正文

多線程基於.NET應用程序迅速響應

2022-06-13   來源: .NET編程 
    用戶不喜歡反應慢的程序程序反應越慢就越沒有用戶會喜歡它在執行耗時較長的操作時使用多線程是明智之舉它可以提高程序 UI 的響應速度使得一切運行顯得更為快速在 Windows 中進行多線程編程曾經是 C++ 開發人員的專屬特權但是現在可以使用所有兼容 Microsoft NET 的語言來編寫其中包括 Visual BasicNET不過Windows 窗體對線程的使用強加了一些重要限制本文將對這些限制進行闡釋並說明如何利用它們來提供快速高質量的 UI 體驗即使是程序要執行的任務本身速度就較慢

為什麼選擇多線程?

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

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

  執行這種操作期間 UI 凍結的原因在於UI 是個單線程程序單線程不可能在等待 CDROM驅動器讀取操作的同時處理用戶輸入如圖 所示打開文件對話框會調用某些阻塞 (blocking) API 來確定 CDROM 的標題阻塞 API 在未完成自己的工作之前不會返回因此這期間它會阻止線程做其他事情

  






   單線程




  在多線程下像這樣耗時較長的任務就可以在其自己的線程中運行這些線程通常稱為輔助線程因為只有輔助線程受到阻止所以阻塞操作不再導致用戶界面凍結如圖 所示應用程序的主線程可以繼續處理用戶的鼠標和鍵盤輸入的同時受阻的另一個線程將等待 CDROM 讀取或執行輔助線程可能做的任何操作



  




   多線程




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

  如果鼠標單擊和相應的 UI 提示(例如重新繪制按鈕)之間的延遲超過 ms那麼操作與顯示之間就會稍顯不連貫並因此產生如同影片斷幀那樣令人心煩的感覺為了達到完全高質量的響應效果上限必須是 ms另一方面如果您確實不介意感覺稍顯不連貫但也不想因為停頓過長而激怒用戶則可按照通常用戶所能容忍的限度將該間隔設為 ms

  這意味著如果想讓用戶界面保持響應迅速則任何阻塞操作都應該在輔助線程中執行 — 不管是機械等待某事發生(例如等待 CDROM 啟動或者硬盤定位數據)還是等待來自網絡的響應

異步委托調用

  在輔助線程中運行代碼的最簡單方式是使用異步委托調用(所有委托都提供該功能)委托通常是以同步方式進行調用在調用委托時只有包裝方法返回後該調用才會返回要以異步方式調用委托請調用 BeginInvoke 方法這樣會對該方法排隊以在系統線程池的線程中運行調用線程會立即返回而不用等待該方法完成這比較適合於 UI 程序因為可以用它來啟動耗時較長的作業而不會使用戶界面反應變慢

  例如在以下代碼中SystemWindowsFormsMethodInvoker 類型是一個系統定義的委托用於調用不帶參數的方法

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

  如果想要傳遞參數可以選擇合適的系統定義的委托類型或者自己來定義委托MethodInvoker 委托並沒有什麼神奇之處和其他委托一樣調用 BeginInvoke 會使該方法在系統線程池的線程中運行而不會阻塞 UI 線程以便其可執行其他操作對於以上情況該方法不返回數據所以啟動它後就不用再去管它如果您需要該方法返回的結果則 BeginInvoke 的返回值很重要並且您可能不傳遞空參數然而對於大多數 UI 應用程序而言這種啟動後就不管的風格是最有效的稍後會對原因進行簡要討論您應該注意到BeginInvoke 將返回一個 IAsyncResult這可以和委托的 EndInvoke 方法一起使用以在該方法調用完畢後檢索調用結果

  還有其他一些可用於在另外的線程上運行方法的技術例如直接使用線程池 API 或者創建自己的線程然而對於大多數用戶界面應用程序而言有異步委托調用就足夠了采用這種技術不僅編碼容易而且還可以避免創建並非必需的線程因為可以利用線程池中的共享線程來提高應用程序的整體性能

線程和控件

  Windows 窗體體系結構對線程使用制定了嚴格的規則如果只是編寫單線程應用程序則沒必要知道這些規則這是因為單線程的代碼不可能違反這些規則然而一旦采用多線程就需要理解 Windows 窗體中最重要的一條線程規則除了極少數的例外情況否則都不要在它的創建線程以外的線程中使用控件的任何成員

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

  // Created on UI thread private Label lblStatus; // Doesnt run on UI thread private void RunsOnWorkerThread() { DoSomethingSlow(); lblStatusText = Finished!; // BAD!! }

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

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

在正確的線程中調用控件

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

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

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

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

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

包裝 ControlInvoke

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

  ShowProgress 方法對將調用引向正確線程的工作進行封裝這意味著輔助線程代碼不再擔心需要過多關注 UI 細節而只要定期調用 ShowProgress 即可請注意我定義了自己的方法該方法違背了必須在 UI 線程上進行調用這一規則因為它進而只調用不受該規則約束的其他方法這種技術會引出一個較為常見的話題為什麼不在控件上編寫公共方法呢(這些方法記錄為 UI 線程規則的例外)?

  剛好 Control 類為這樣的方法提供了一個有用的工具如果我提供一個設計為可從任何線程調用的公共方法則完全有可能某人會從 UI 線程調用這個方法在這種情況下沒必要調用 BeginInvoke因為我已經處於正確的線程中調用 Invoke 完全是浪費時間和資源不如直接調用適當的方法為了避免這種情況Control 類將公開一個稱為 InvokeRequired 的屬性這是只限 UI 線程規則的另一個例外它可從任何線程讀取如果調用線程是 UI 線程則返回假其他線程則返回真這意味著我可以按以下方式修改包裝

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

  ShowProgress 現在可以記錄為可從任何線程調用的公共方法這並沒有消除復雜性 — 執行 BeginInvoke 的代碼依然存在它還占有一席之地不幸的是沒有簡單的方法可以完全擺脫它

鎖定

  任何並發系統都必須面對這樣的事實兩個線程可能同時試圖使用同一塊數據有時這並不是問題 — 如果多個線程在同一時間試圖讀取某個對象中的某個字段則不會有問題然而如果有線程想要修改該數據就會出現問題如果線程在讀取時剛好另一個線程正在寫入則讀取線程有可能會看到虛假值如果兩個線程在同一時間在同一個位置執行寫入操作則在同步寫入操作發生之後所有從該位置讀取數據的線程就有可能看到一堆垃圾數據雖然這種行為只在特定情況下才會發生讀取操作甚至不會與寫入操作發生沖突但是數據可以是兩次寫入結果的混加並保持錯誤結果直到下一次寫入值為止為了避免這種問題必須采取措施來確保一次只有一個線程可以讀取或寫入某個對象的狀態

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

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

  請注意MoveBy 方法中的邏輯受同樣的鎖語句保護當所做的修改比簡單的讀取或寫入更復雜時整個過程必須由單獨的鎖語句保護這也適用於對多個字段進行更新 — 在對象處於一致狀態之前一定不能釋放該鎖如果該鎖在更新狀態的過程中釋放則其他線程也許能夠獲得它並看到不一致狀態如果您已經擁有一個鎖並調用一個試圖獲取該鎖的方法則不會導致問題出現因為單獨線程允許多次獲得同一個鎖對於需要鎖定以保護對字段的低級訪問和對字段執行的高級操作的代碼這非常重要MoveBy 使用 Position 屬性它們同時獲得該鎖只有最外面的鎖阻塞完成後該鎖才會恰當地釋放

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

死鎖

  鎖是確保多線程代碼正常運行的基本條件即使它們本身也會引入新的風險在另一個線程上運行代碼的最簡單方式是使用異步委托調用(請參見圖

  如果曾經調用過 Foo 的 CallBar 方法則這段代碼會慢慢停止運行CallBar 方法將獲得 Foo 對象上的鎖並直到 BarWork 返回後才釋放它然後BarWork 使用異步委托調用在某個線程池線程中調用 Foo 對象的 FooWork 方法接下來它會在調用委托的 EndInvoke 方法前執行一些其他操作EndInvoke 將等待輔助線程完成但輔助線程卻被阻塞在 FooWork 中它也試圖獲取 Foo 對象的鎖但鎖已被 CallBar 方法持有所以FooWork 會等待 CallBar 釋放鎖但 CallBar 也在等待 BarWork 返回不幸的是BarWork 將等待 FooWork 完成所以 FooWork 必須先完成它才能開始結果沒有線程能夠進行下去

  這就是一個死鎖的例子其中有兩個或更多線程都被阻塞以等待對方進行這裡的情形和標准死鎖情況還是有些不同後者通常包括兩個鎖這表明如果有某個因果性(過程調用鏈)超出線程界限就會發生死鎖即使只包括一個鎖!ControlInvoke 是一種跨線程調用過程的方法這是個不爭的重要事實BeginInvoke 不會遇到這樣的問題因為它並不會使因果性跨線程實際上它會在某個線程池線程中啟動一個全新的因果性以允許原有的那個獨立進行然而如果保留 BeginInvoke 返回的 IAsyncResult並用它調用 EndInvoke則又會出現問題因為 EndInvoke 實際上已將兩個因果性合二為一避免這種情況的最簡單方法是當持有一個對象鎖時不要等待跨線程調用完成要確保這一點應該避免在鎖語句中調用 Invoke 或 EndInvoke其結果是當持有一個對象鎖時將無需等待其他線程完成某操作要堅持這個規則說起來容易做起來難

  在檢查代碼的 BarWork 時它是否在鎖語句的作用域內並不明顯因為在該方法中並沒有鎖語句出現這個問題的唯一原因是 BarWork 調用自 FooCallBar 方法的鎖語句這意味著只有確保正在調用的函數並不擁有鎖時調用 ControlInvoke 或 EndInvoke 才是安全的對於非私有方法而言確保這一點並不容易所以最佳規則是根本不調用 ControlInvoke 和 EndInvoke這就是為什麼啟動後就不管的編程風格更可取的原因也是為什麼 ControlBeginInvoke 解決方案通常比 ControlInvoke 解決方案好的原因

  有時候除了破壞規則別無選擇這種情況下就需要仔細嚴格地分析但只要可能在持有鎖時就應該避免阻塞因為如果不這樣死鎖就難以消除

使其簡單

  如何既從多線程獲益最大又不會遇到困擾並發代碼的棘手錯誤呢?如果提高的 UI 響應速度僅僅是使程序時常崩潰那麼很難說是改善了用戶體驗大部分在多線程代碼中普遍存在的問題都是由要一次運行多個操作的固有復雜性導致的這是因為大多數人更善於思考連續過程而非並發過程通常最好的解決方案是使事情盡可能簡單

  UI 代碼的性質是它從外部資源接收事件如用戶輸入它會在事件發生時對其進行處理但卻將大部分時間花在了等待事件的發生如果可以構造輔助線程和 UI 線程之間的通信使其適合該模型則未必會遇到這麼多問題因為不會再有新的東西引入我是這樣使事情簡單化的將輔助線程視為另一個異步事件源如同 Button 控件傳遞諸如 Click 和 MouseEnter 這樣的事件可以將輔助線程視為傳遞事件(如 ProgressUpdate 和 WorkComplete)的某物只是簡單地將這看作一種類比還是真正將輔助對象封裝在一個類中並按這種方式公開適當的事件這完全取決於您後一種選擇可能需要更多的代碼但會使用戶界面代碼看起來更加統一不管哪種情況都需要 ControlBeginInvoke 在正確的線程上傳遞這些事件

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

  例如讓輔助線程中途發起對話以請求完成結果需要的信息將非常困難如果確實需要這樣做也最好是在輔助線程中發起這樣的對話而不要在主 UI 線程中發起該約束是有利的因為它將確保有一個非常簡單且適用於兩線程間通信的模型 — 在這裡簡單是成功的關鍵這種開發風格的優勢在於在等待另一個線程時不會出現線程阻塞這是避免死鎖的有效策略

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

取消

  前面示例所帶來的問題是要取消操作只能通過退出整個應用程序實現雖然在讀取某個目錄時 UI 仍然保持迅速響應但由於在當前操作完成之前程序將禁用相關按鈕所以用戶無法查看另一個目錄如果試圖讀取的目錄是在一台剛好沒有響應的遠程機器上這就很不幸因為這樣的操作需要很長時間才會超時

  要取消一個操作也比較困難盡管這取決於怎樣才算取消一種可能的理解是停止等待這個操作完成並繼續另一個操作這實際上是拋棄進行中的操作並忽略最終完成時可能產生的後果對於當前示例這是最好的選擇因為當前正在處理的操作(讀取目錄內容)是通過調用一個阻塞 API 來執行的取消它沒有關系但即使是如此松散的假取消也需要進行大量工作如果決定啟動新的讀取操作而不等待原來的操作完成則無法知道下一個接收到的通知是來自這兩個未處理請求中的哪一個

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

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

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

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

程序關閉

  某些情況下可以采用啟動後就不管的異步操作而不需要其他復雜要求來使操作可取消然而即使用戶界面不要求取消有可能還是需要實現這項功能以使程序可以徹底關閉

  當應用程序退出時如果由線程池創建的輔助線程還在運行則這些線程會被終止終止是簡單粗暴的操作因為關閉甚至會繞開任何還起作用的 Finally 塊如果異步操作執行的某些工作不應該以這種方式被打斷則必須確保在關閉之前這樣的操作已經完成此類操作可能包括對文件執行的寫入操作但由於突然中斷後文件可能被破壞

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

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

錯誤處理

  在輔助線程中出現的錯誤一般可以通過觸發 UI 線程中的事件來處理這樣錯誤處理方式就和完成及進程更新方式完全一樣因為很難在輔助線程上進行錯誤恢復所以最簡單的策略就是讓所有錯誤都為致命錯誤錯誤恢復的最佳策略是使操作完全失敗並在 UI 線程上執行重試邏輯如果需要用戶干涉來修復造成錯誤的問題簡單的做法是給出恰當的提示

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

小結

  謹慎地使用多線程代碼可以使 UI 在執行耗時較長的任務時不會停止響應從而顯著提高應用程序的反應速度異步委托調用是將執行速度緩慢的代碼從 UI 線程遷移出來從而避免此類間歇性無響應的最簡單方式

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

  有時需要讓 UI 向一個正在處理的操作發送消息其中最常見的是取消一個操作通過建立一個表示正在進行的調用的對象並維護由輔助線程定期檢查的取消標記可實現這一目的如果用戶界面線程需要等待取消被認可(因為用戶需要知道工作已確實終止或者要求徹底退出程序)實現起來會有些復雜但所提供的示例代碼中包含了一個將所有復雜性封裝在內的基類派生類只需要執行一些必要的工作周期性測試取消以及要是因為取消請求而停止工作就將結果通知基類


From:http://tw.wingwit.com/Article/program/net/201311/11788.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.