C#中的线程(三) 使用多线程
第三部分:使用多線程
?
1.? 單元模式和Windows Forms
?????? 單元模式線程是一個自動線程安全機制, 非常貼近于COM——Microsoft的遺留下的組件對象模型。盡管.NET最大地放棄擺脫了遺留下的模型,但很多時候它也會突然出現,這是因為有必要與舊的API 進行通信。單元模式線程與Windows Forms最相關,因為大多Windows Forms使用或包裝了長期存在的Win32 API——連同它的單元傳統。
?????? 單元是多線程的邏輯上的“容器”,單元產生兩種容量——“單的”和“多的”。單線 程單元只包含一個線程;多線程單元可以包含任何數量的線程。單線程模式更普遍 并且能與兩者有互操作性。
?????? 就像包含線程一樣,單元也包含對象,當對象在一個單元內被創建后,在它的生命周期中它將一直存在在那,永遠也“居家不出”地與那些駐留線程在一起。這類似于被包含在.NET 同步環境中 ,除了同步環境中沒有自己的或包含線程。任何線程可以訪問在任何同步環境中的對象 ——在排它鎖的控制中。但是單元內的對象只有單元內的線程才可以訪問。
?????? 想象一個圖書館,每本書都象征著一個對象;借出書是不被允許的,書都在圖書館 創建并直到它壽終正寢。此外,我們用一個人來象征一個線程。
?????? 一個同步內容的圖書館允許任何人進入,同時同一時刻只允許一個人進入,在圖書館外會形成隊列。
?????? 單元模式的圖書館有常駐維護人員——對于單線程模式的圖書館有一個圖書管理員, 對于多線程模式的圖書館則有一個團隊的管理員。沒人被允許除了隸屬與維護人員的人 ——資助人想要完成研究就必須給圖書管理員發信號,然后告訴管理員去做工作!給管理員發信號被稱為調度編組——資助人通過調度把方法依次讀出給一個隸屬管理員的人(或,某個隸屬管理員的人!)。 調度編組是自動的,在Windows Forms通過信息泵被實現在庫結尾。這就是操作系統經常檢查鍵盤和鼠標的機制。如果信息到達的太快了,以致不能被處理,它們將形成消息隊列,所以它們可以以它們到達的順序被處理。
?
1.1? 定義單元模式
?
??????? .NET線程在進入單元核心Win32或舊的COM代碼前自動地給單元賦值,它被默認地指定為多線程單元模式,除非需要一個單線程單元模式,就像下面的一樣:
?| 1 2 | Thread t = new Thread (...); t.SetApartmentState (ApartmentState.STA); |
??????? 你也可以用STAThread特性標在主線程上來讓它與單線程單元相結合:
?| 1 2 3 4 | class Program { ??[STAThread] static void Main() { ??... |
??????? 線程單元設置對純.NET代碼沒有效果,換言之,即使兩個線程都有STA 的單元狀態,也可以被相同的對象同時調用相同的方法,就沒有自動的信號編組或鎖定發生了, 只有在執行非托管的代碼時,這才會發生。
在System.Windows.Forms名稱空間下的類型,廣泛地調用Win32代碼, 在單線程單元下工作。由于這個原因,一個Windos Forms程序應該在它的主方法上貼上 [STAThread]特性,除非在執行Win32 UI代碼之前以下二者之一發生了:
- 它將調度編組成一個單線程單元
- 它將崩潰
?
1.2? Control.Invoke
?
在多線程的Windows Forms程序中,通過非創建控件的線程調用控件的的屬性和方法是非法的。所有跨進程的調用必須被明確地排列至創建控件的線程中(通常為主線程),利用Control.Invoke 或 Control.BeginInvoke方法。你不能依賴自動調度編組因為它發生的太晚了,僅當執行剛好進入了非托管的代碼它才發生,而.NET已有足夠的時間來運行“錯誤的”線程代碼,那些非線程安全的代碼。
一個優秀的管理Windows Forms程序的方案是使用BackgroundWorker, 這個類包裝了需要報道進度和完成度的工作線程,并自動地調用Control.Invoke方法作為需要。
?
1.3? BackgroundWorker
?
BackgroundWorker是一個在System.ComponentModel命名空間 下幫助類,它管理著工作線程。它提供了以下特性:
- "cancel" 標記,對于給工作線程打信號讓它結束而沒有使用 Abort的情況
- 提供報道進度,完成度和退出的標準方案
- 實現了IComponent接口,允許它參與Visual Studio設計器
- 在工作線程之上做異常處理
- 更新Windows Forms控件以應答工作進度或完成度的能力
???? 最后兩個特性是相當地有用:意味著你不再需要將try/catch語句塊放到 你的工作線程中了,并且更新Windows Forms控件不需要調用 Control.Invoke了。BackgroundWorker使用線程池工作, 對于每個新任務,它循環使用避免線程們得到休息。這意味著你不能在 BackgroundWorker線程上調用 Abort了。
???? 下面是使用BackgroundWorker最少的步驟:
- 實例化 BackgroundWorker,為DoWork事件增加委托。
- 調用RunWorkerAsync方法,使用一個隨便的object參數。
???? 這就設置好了它,任何被傳入RunWorkerAsync的參數將通過事件參數的Argument屬性,傳到DoWork事件委托的方法中,下面是例子:
?| 1 2 3 4 5 6 7 8 9 10 11 12 | class Program { s?? tatic BackgroundWorker bw = new BackgroundWorker(); static void Main() { ????????bw.DoWork += bw_DoWork; ????????bw.RunWorkerAsync ("Message to worker");???? ????Console.ReadLine(); ??} static void bw_DoWork (object sender, DoWorkEventArgs e) { // 這被工作線程調用 ????Console.WriteLine (e.Argument);??????? // 寫"Message to worker" ????// 執行耗時的任務... ??} |
????? BackgroundWorker也提供了RunWorkerCompleted事件,它在DoWork事件完成后觸發,處理RunWorkerCompleted事件并不是強制的,但是為了查詢到DoWork中的異常,你通常會這么做的。RunWorkerCompleted中的代碼可以更新Windows Forms 控件,而不用顯示的信號編組,而DoWork中就可以這么做。
添加進程報告支持:
- 設置WorkerReportsProgress屬性為true
- 在DoWork中使用“完成百分比”周期地調用ReportProgress方法,以及可選用戶狀態對象
- 處理ProgressChanged事件,查詢它的事件參數的 ProgressPercentage屬性
????? ProgressChanged中的代碼就像RunWorkerCompleted一樣可以自由地與UI控件進行交互,這在更性進度欄尤為有用。
添加退出報告支持:
- 設置WorkerSupportsCancellation屬性為true
- 在DoWork中周期地檢查CancellationPending屬性:如果為true,就設置事件參數的Cancel屬性為true,然后返回。(工作線程可能會設置Cancel為true,并且不通過CancellationPending進行提示——如果判定工作太過困難并且它不能繼續運行)
- 調用CancelAsync來請求退出
下面的例子實現了上面描述的特性:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | using System; using System.Threading; using System.ComponentModel; ?? class Program { ??static BackgroundWorker bw; ??static void Main() { ????bw = new BackgroundWorker(); ????bw.WorkerReportsProgress = true; ????bw.WorkerSupportsCancellation = true; ????bw.DoWork += bw_DoWork; ????bw.ProgressChanged += bw_ProgressChanged; ????bw.RunWorkerCompleted += bw_RunWorkerCompleted; ?? ????bw.RunWorkerAsync ("Hello to worker"); ????? ????Console.WriteLine ("Press Enter in the next 5 seconds to cancel"); ????Console.ReadLine(); ????if (bw.IsBusy) bw.CancelAsync(); ????Console.ReadLine(); ??} ?? ??static void bw_DoWork (object sender, DoWorkEventArgs e) { ????for (int i = 0; i <= 100; i += 20) { ??????if (bw.CancellationPending) { ????????e.Cancel = true; ????????return; ??????} ??????bw.ReportProgress (i); ??????Thread.Sleep (1000); ????} ????e.Result = 123;??? // This gets passed to RunWorkerCompleted ??} ?? ??static void bw_RunWorkerCompleted (object sender, ??RunWorkerCompletedEventArgs e) { ????if (e.Cancelled) ??????Console.WriteLine ("You cancelled!"); ????else if (e.Error != null) ??????Console.WriteLine ("Worker exception: " + e.Error.ToString()); ????else ??????Console.WriteLine ("Complete - " + e.Result);????? // from DoWork ??} ?? ??static void bw_ProgressChanged (object sender, ??ProgressChangedEventArgs e) { ????Console.WriteLine ("Reached " + e.ProgressPercentage + "%"); ??} } |
?
1.4? BackgroundWorker的子類
??
?????? BackgroundWorker不是密封類,它提供OnDoWork為虛方法,暗示著另一個模式可以它。 當寫一個可能耗時的方法,你可以或最好寫個返回BackgroundWorker子類的等方法,預配置完成異步的工作。使用者只要處理RunWorkerCompleted事件和ProgressChanged事件。比如,設想我們寫一個耗時 的方法叫做GetFinancialTotals:
?| 1 2 3 4 5 | public class Client { ??Dictionary <string,int> GetFinancialTotals (int foo, int bar) { ... } ??... } |
????? 我們可以如此來實現:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public class Client { ??public FinancialWorker GetFinancialTotalsBackground (int foo, int bar) { ????return new FinancialWorker (foo, bar); ??} } ?? public class FinancialWorker : BackgroundWorker { ??public Dictionary <string,int> Result;?? // We can add typed fields. ??public volatile int Foo, Bar;??????????? // We could even expose them ???????????????????????????????????????????// via properties with locks! ??public FinancialWorker() { ????WorkerReportsProgress = true; ????WorkerSupportsCancellation = true; ??} ?? ??public FinancialWorker (int foo, int bar) : this() { ????this.Foo = foo; this.Bar = bar; ??} ?? ??protected override void OnDoWork (DoWorkEventArgs e) { ????ReportProgress (0, "Working hard on this report..."); ????Initialize financial report data ?? ????while (!finished report ) { ??????if (CancellationPending) { ????????e.Cancel = true; ????????return; ??????} ??????Perform another calculation step ??????ReportProgress (percentCompleteCalc, "Getting there..."); ????}????? ????ReportProgress (100, "Done!"); ????e.Result = Result = completed report data; ??} } |
???
????? 無論誰調用GetFinancialTotalsBackground都會得到一個FinancialWorker——一個用真實地可用地包裝了管理后臺操作。它可以報告進度,被取消,與Windows Forms交互而不用使用Control.Invoke。它也有異常句柄,并且使用了標準的協議(與使用BackgroundWorker沒任何區別!)
???? 這種BackgroundWorker的用法有效地回避了舊有的“基于事件的異步模式”。
?
2? ReaderWriterLockSlim類
?
???? //注意還有一個老的ReaderWriterLock類,Slim類為.net 3.5新增,提高了性能。
???? 通常來講,一個類型的實例對于并行的讀操作是線程安全的,但是并行地更新操作則不是(并行地讀與更新也不是)。 這對于資源(比如一個文件)也是一樣的。使用一個簡單的獨占鎖來鎖定所有可能的訪問能夠解決實例的線程安全為問題,但是當有很多的讀操作而只是偶然的更新操作的時候,這就很不合理的限制了并發。一個例子就是這在一個業務程序服務器中,為了快速查找把數據緩存到靜態字段中。在這樣的情況下,ReaderWriterLockSlim類被設計成提供最大可能的鎖定。
???? ReaderWriterLockSlim有兩種基本的Lock方法:一個獨占的Wirte Lock ,和一個與其他Read lock相容的讀鎖定。
???? 所以,當一個線程擁有一個Write Lock的時候,會阻塞所有其他線程獲得讀寫鎖。但是當沒有線程獲得WriteLock時,可以有多個線程同時獲得ReadLock,進行讀操作。
???? ReaderWriterLockSlim提供了下面四個方法來得到和釋放讀寫鎖:
?| 1 2 3 4 | public void EnterReadLock(); public void ExitReadLock(); public void EnterWriteLock(); public void ExitWriteLock(); |
?
???? 另外對于所有的EnterXXX方法,還有”Try”版本的方法,它們接收timeOut參數,就像Monitor.TryEnter一樣(在資源爭用嚴重的時候超時發生相當容易)。另外ReaderWriterLock提供了其他類似的AcquireXXX 和 ReleaseXXX方法,它們超時退出的時候拋出異常而不是返回false。
?????? 下面的程序展示了ReaderWriterLockSlim——三個線程循環地枚舉一個List,同時另外兩個線程每一秒鐘添加一個隨機數到List中。一個read lock保護List的讀取線程,同時一個write lock保護寫線程。
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class SlimDemo { ??static ReaderWriterLockSlim rw = new ReaderWriterLockSlim(); ??static List<int> items = new List<int>(); ??static Random rand = new Random(); ??static void Main() ??{ ????new Thread (Read).Start(); ????new Thread (Read).Start(); ????new Thread (Read).Start(); ????new Thread (Write).Start ("A"); ????new Thread (Write).Start ("B"); ??} ??static void Read() ??{ ????while (true) ????{ ??????rw.EnterReadLock(); ??????foreach (int i in items) Thread.Sleep (10); ??????rw.ExitReadLock(); ????} ??} ??static void Write (object threadID) ??{ ????while (true) ????{?????????????? ??????int newNumber = GetRandNum (100); ??????rw.EnterWriteLock(); ??????items.Add (newNumber); ??????rw.ExitWriteLock(); ??????Console.WriteLine ("Thread " + threadID + " added " + newNumber); ??????Thread.Sleep (100); ????} ??} ??static int GetRandNum (int max) { lock (rand) return rand.Next (max); } } <em><span style="font-family: YaHei Consolas Hybrid;">//在實際的代碼中添加try/finally,保證異常情況寫lock也會被釋放。</span></em> |
結果為:
Thread B added 61 Thread A added 83 Thread B added 55 Thread A added 33 ...????? ReaderWriterLockSlim比簡單的Lock允許更大的并發讀能力。我們能夠添加一行代碼到Write方法,在While循環的開始:
?| 1 | Console.WriteLine (rw.CurrentReadCount + " concurrent readers"); |
?????? 基本上總是會返回“3 concurrent readers”(讀方法花費了更多的時間在Foreach循環),ReaderWriterLockSlim還提供了許多與CurrentReadCount屬性類似的屬性來監視lock的情況:
?| 1 2 3 4 5 6 7 8 9 10 11 | public bool IsReadLockHeld??????????? { get; } public bool IsUpgradeableReadLockHeld { get; } public bool IsWriteLockHeld?????????? { get; } public int? WaitingReadCount????????? { get; } public int? WaitingUpgradeCount?????? { get; } public int? WaitingWriteCount???????? { get; } public int? RecursiveReadCount??????? { get; } public int? RecursiveUpgradeCount???? { get; } public int? RecursiveWriteCount?????? { get; } |
????? 有時候,在一個原子操作里面交換讀寫鎖是非常有用的,比如,當某個item不在list中的時候,添加此item進去。最好的情況是,最小化寫如鎖的時間,例如像下面這樣處理:
??? 1 獲得一個讀取鎖
??? 2 測試list是否包含item,如果是,則返回
??? 3 釋放讀取鎖
??? 4 獲得一個寫入鎖
??? 5 寫入item到list中,釋放寫入鎖。
???? 但是在步驟3、4之間,當另外一個線程可能偷偷修改List(比如說添加同樣一個Item),ReaderWriterLockSlim通過提供第三種鎖來解決這個問題,這就是upgradeable lock。一個可升級鎖和read lock 類似,只是它能夠通過一個原子操作,被提升為write lock。使用方法如下:
????? 從調用者的角度,這非常想遞歸(嵌套)鎖。實際上第三步的時候,通過一個原子操作,釋放了read lock 并獲得了一個新的write lock.
????? upgradeable locks 和read locks之間另外還有一個重要的區別,盡管一個upgradeable locks 能夠和任意多個read locks共存,但是一個時刻,只能有一個upgradeable lock自己被使用。這防止了死鎖。這和SQL Server的Update lock類似
????? 我們可以改變前面例子的Write方法來展示upgradeable lock:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | while (true) { ??int newNumber = GetRandNum (100); ??rw.EnterUpgradeableReadLock(); ??if (!items.Contains (newNumber)) ??{ ????rw.EnterWriteLock(); ????items.Add (newNumber); ????rw.ExitWriteLock(); ????Console.WriteLine ("Thread " + threadID + " added " + newNumber); ??} ??rw.ExitUpgradeableReadLock(); ??Thread.Sleep (100); } |
ReaderWriterLock 沒有提供upgradeable locks的功能。
?
2.1? 遞歸鎖 Lock recursion
Ordinarily, nested or recursive locking is prohibited with ReaderWriterLockSlim. Hence, the following throws an exception:
默認情況下,遞歸(嵌入)鎖被ReaderWriterLockSlim禁止,因為下面的代碼可能拋出異常。
?| 1 2 3 4 5 | var rw = new ReaderWriterLockSlim(); rw.EnterReadLock(); rw.EnterReadLock(); rw.ExitReadLock(); rw.ExitReadLock(); |
但是顯示地聲明允許嵌套的話,就能正常工作,不過這帶來了不必要的復雜性。
?| 1 | var rw = new ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion); |
?
?| 1 2 3 4 5 6 | rw.EnterWriteLock(); rw.EnterReadLock(); Console.WriteLine (rw.IsReadLockHeld);???? // True Console.WriteLine (rw.IsWriteLockHeld);??? // True rw.ExitReadLock(); rw.ExitWriteLock(); |
?? 使用鎖的順序大致為:Read Lock? -->? Upgradeable Lock? -->? Write Lock
?
3?? 線程池
?
??????? 如果你的程序有很多線程,導致花費了大多時間在等待句柄的阻止上,你可以通過 線程池來削減負擔。線程池通過合并很多等待句柄在很少的線程上來節省時間。
??????? 使用線程池,你需要注冊一個連同將被執行的委托的Wait Handle,在Wait Handle發信號時。這個工作通過調用ThreadPool.RegisterWaitForSingleObject來完成,如下:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Test { ??static ManualResetEvent starter = new ManualResetEvent (false); ?? ??public static void Main() { ????ThreadPool.RegisterWaitForSingleObject (starter, Go, "hello", -1, true); ????Thread.Sleep (5000); ????Console.WriteLine ("Signaling worker..."); ????starter.Set(); ????Console.ReadLine(); ??} ?? ??public static void Go (object data, bool timedOut) { ????Console.WriteLine ("Started " + data); ????// Perform task... ??} } |
????? 除了等待句柄和委托之外,RegisterWaitForSingleObject也接收一個“黑盒”對象,它被傳遞到你的委托方法中( 就像用ParameterizedThreadStart一樣),擁有一個毫秒級的超時參數(-1意味著沒有超時)和布爾標志來指明請求是一次性的還是循環的。
?????? 所有進入線程池的線程都是后臺的線程,這意味著 它們在程序的前臺線程終止后將自動的被終止。但你如果想等待進入線程池的線程都完成它們的重要工作在退出程序之前,在它們上調用Join是不行的,因為進入線程池的線程從來不會結束!意思是說,它們被改為循環,直到父進程終止后才結束。所以為知道運行在線程池中的線程是否完成,你必須發信號——比如用另一個Wait Handle。
????? 在線程池中的線程上調用Abort 是一個壞主意,線程需要在程序域的生命周期中循環。
????? 你也可以用QueueUserWorkItem方法而不用等待句柄來使用線程池,它定義了一個立即執行的委托。你不必在多個任務中節省共享線程,但有一個慣例:線程池保持一個線程總數的封頂(默認為25),在任務數達到這個頂值后將自動排隊。這就像程序范圍的有25個消費者的生產者/消費者隊列。在下面的例子中,100個任務入列到線程池中,而一次只執行 25個,主線程使用Wait 和 Pulse來等待所有的任務完成:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Test { ??static object workerLocker = new object (); ??static int runningWorkers = 100; ?? ??public static void Main() { ????for (int i = 0; i < 100; i++) { ??????ThreadPool.QueueUserWorkItem (Go, i); ????} ????Console.WriteLine ("Waiting for threads to complete..."); ????lock (workerLocker) { ??????while (runningWorkers > 0) Monitor.Wait (workerLocker); ????} ????Console.WriteLine ("Complete!"); ????Console.ReadLine(); ??} ?? ??public static void Go (object instance) { ????Console.WriteLine ("Started: " + instance); ????Thread.Sleep (1000); ????Console.WriteLine ("Ended: " + instance); ????lock (workerLocker) { ??????runningWorkers--; Monitor.Pulse (workerLocker); ????} ??} } |
???? 為了傳遞多個對象給目標方法,你可以定義個擁有所有需要屬性的自定義對象,或者調用一個匿名方法。比如如果Go方法接收兩個整型參數,會像下面這樣:
?| 1 | ThreadPool.QueueUserWorkItem (delegate (object notUsed) { Go (23,34); }); |
另一個進入線程池的方式是通過異步委托。
?
4.?? 異步委托
?
???? 在第一部分我們描述如何使用 ParameterizedThreadStart把數據傳入線程中。有時候 你需要通過另一種方式,來從線程中得到它完成后的返回值。異步委托提供了一個便利的機制,允許許多參數在兩個方向上傳遞 。此外,未處理的異常在異步委托中在原始線程上被重新拋出,因此在工作線程上不需要明確的處理了。異步委托也提供了計入 線程池的另一種方式。
???? 對此你必須付出的代價是要跟從異步模型。為了看看這意味著什么,我們首先討論更常見的同步模型。我們假設我們想比較 兩個web頁面,我們按順序取得它們,然后像下面這樣比較它們的輸出:
?| 1 2 3 4 5 6 | static void ComparePages() { ??WebClient wc = new WebClient (); ??string s1 = wc.DownloadString ("http://www.oreilly.com"); ??string s2 = wc.DownloadString ("http://oreilly.com"); ??Console.WriteLine (s1 == s2 ? "Same" : "Different"); } |
??? 如果兩個頁面同時下載當然會更快了。問題在于當頁面正在下載時DownloadString阻止了繼續調用方法。如果我們能 調用 DownloadString在一個非阻止的異步方式中會變的更好,換言之:
1. 我們告訴 DownloadString 開始執行
2. 在它執行時我們執行其它任務,比如說下載另一個頁面
3. 我們詢問DownloadString的所有結果
??? WebClient類實際上提供一個被稱為DownloadStringAsync的內建方法 ,它提供了就像異步函數的功能。而眼下,我們忽略這個問題,集中精力在任何方法都可以被異步調用的機制上。
??? 第三步使異步委托變的有用。調用者匯集了工作線程得到結果和允許任何異常被重新拋出。沒有這步,我們只有普通多線程。雖然也可能不用匯集方式使用異步委托,你可以用ThreadPool.QueueWorkerItem 或 BackgroundWorker。
??? 下面我們用異步委托來下載兩個web頁面,同時實現一個計算:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | delegate string DownloadString (string uri); ?? static void ComparePages() { ?? ??// Instantiate delegates with DownloadString's signature: ??DownloadString download1 = new WebClient().DownloadString; ??DownloadString download2 = new WebClient().DownloadString; ??? ??// Start the downloads: ??IAsyncResult cookie1 = download1.BeginInvoke (uri1, null, null); ??IAsyncResult cookie2 = download2.BeginInvoke (uri2, null, null); ??? ??// Perform some random calculation: ??double seed = 1.23; ??for (int i = 0; i < 1000000; i++) seed = Math.Sqrt (seed + 1000); ??? ??// Get the results of the downloads, waiting for completion if necessary. ??// Here's where any exceptions will be thrown: ??string s1 = download1.EndInvoke (cookie1); ??string s2 = download2.EndInvoke (cookie2); ??? ??Console.WriteLine (s1 == s2 ? "Same" : "Different"); } |
???? 我們以聲明和實例化我們想要異步運行的方法開始。在這個例子中,我們需要兩個委托,每個引用不同的WebClient的對象(WebClient 不允許并行的訪問,如果它允許,我們就只需一個委托了)。
????? 我們然后調用BeginInvoke,這開始執行并立刻返回控制器給調用者。依照我們的委托,我們必須傳遞一個字符串給 BeginInvoke (編譯器由生產BeginInvoke 和 EndInvoke在委托類型強迫實現這個).
?????? BeginInvoke 還需要兩個參數:一個可選callback和數據對象;它們通常不需要而被設置為null, BeginInvoke返回一個 IASynchResult對象,它擔當著調用 EndInvoke所用的數據。IASynchResult 同時有一個IsCompleted屬性來檢查進度。
?????? 之后我們在委托上調用EndInvoke ,得到需要的結果。如果有必要,EndInvoke會等待, 直到方法完成,然后返回方法返回的值作為委托指定的(這里是字符串)。 EndInvoke一個好的特性是DownloadString有任何的引用或輸出參數, 它們會在 EndInvoke結構賦值,允許通過調用者多個值被返回。
??????? 在異步方法的執行中的任何點發生了未處理的異常,它會重新在調用線程在EndInvoke中拋出。 這提供了精簡的方式來管理返回給調用者的異常。
??????? 如果你異步調用的方法沒有返回值,你也(理論上)應該調用EndInvoke,在部分意義上 在開放了誤判;MSDN上辯論著這個話題。如果你選擇不調用EndInvoke,你需要考慮在工作方法中的異常。
4.1? 異步方法?
????? .NET Framework 中的一些類型提供了某些它們方法的異步版本,它們使用"Begin" 和 "End"開頭。它們被稱之為異步方法,它們有與異步委托類似的特性,但異步委托存在著一些待解決的困難的問題:允許比你所擁有的線程還多的并發活動率。 比如一個web或TCP Socket服務器,如果用NetworkStream.BeginRead? 和 NetworkStream.BeginWrite 來寫的話,就可能在僅僅線程池線程中處理數百個并發的請求。
????? 除非你正在寫一個專門的高并發程序,否則不應該過多地使用異步方法。理由如下:
- 不像異步委托,異步方法實際上可能沒有與調用者同時執行
- 如果你未能小心翼翼地遵從它的模式異步方法的好處被侵腐或消失了,
- 當你恰當地遵從了它的模式,事情立刻變的復雜了
???? 如果你只是像簡單地獲得并行執行的結果,你最好遠離調用異步版本的方法(比如NetworkStream.Read) 而通過異步委托。另一個選項是使用ThreadPool.QueueUserWorkItem或BackgroundWorker,又或者只是簡單地創建新的線程。
?
4.2?? 異步事件
?
????? 另一種模式存在,就是為什么類型可以提供異步版本的方法。這就是所謂的“基于事件的異步模式”,這些的方法以"Async"結束,相對應的事件以"Completed"結束。WebClient使用這個模式在它的DownloadStringAsync 方法中。 為了使用它,你要首先處理"Completed" 事件(例如:DownloadStringCompleted),然后調用"Async"方法(例如:DownloadStringAsync)。當方法完成后,它調用你事件句柄。不幸的是,WebClient的實現是有缺陷的:像DownloadStringAsync 這樣的方法對于下載的一部分時間阻止了調用者的線程。
???? 基于事件的模式也提供了報道進度和取消操作,被友好地設計成可對Windows程序可更新forms和控件。如果在某個類型中你需要這些特性 ,而它卻不支持(或支持的不好)基于事件的模式,你沒必要去自己實現它(你也根本不想去做!)。盡管如此,所有的這些通過 BackgroundWorker這個幫助類便可輕松完成。
?
5. 計時器
?
???? 周期性的執行某個方法最簡單的方法就是使用一個計時器,比如System.Threading 命名空間下Timer類。線程計時器利用了線程池,允許多個計時器被創建而沒有額外的線程開銷。 Timer 算是相當簡易的類,它有一個構造器和兩個方法(這對于極簡主義者來說是最高興不過的了)。
?| 1 2 3 4 5 6 7 8 9 | public sealed class Timer : MarshalByRefObject, IDisposable { ??public Timer (TimerCallback tick, object state, 1st, subsequent); ??public bool Change (1st, subsequent);?? // To change the interval ??public void Dispose();??????????????? // To kill the timer } 1st = time to the first tick in milliseconds or a TimeSpan subsequent = subsequent intervals in milliseconds or a TimeSpan ?(use Timeout.Infinite for a one-off callback) |
???
???? 接下來這個例子,計時器5秒鐘之后調用了Tick 的方法,它寫"tick...",然后每秒寫一個,直到用戶敲 Enter:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | using System; using System.Threading; ?? class Program { ??static void Main() { ????Timer tmr = new Timer (Tick, "tick...", 5000, 1000); ????Console.ReadLine(); ????tmr.Dispose();???????? // End the timer ??} ?? ??static void Tick (object data) { ????// This runs on a pooled thread ????Console.WriteLine (data);????????? // Writes "tick..." ??} } |
???? .NET framework在System.Timers命名空間下提供了另一個計時器類。它完全包裝自System.Threading.Timer,在使用相同的線程池時提供了額外的便利——相同的底層引擎。下面是增加的特性的摘要:
- 實現了Component,允許它被放置到Visual Studio設計器中
- Interval屬性代替了Change方法
- Elapsed 事件代替了callback委托
- Enabled屬性開始或暫停計時器
- 提夠Start 和 Stop方法,萬一對Enabled感到迷惑
- AutoReset標志來指示是否循環(默認為true)
例子:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System; using System.Timers;?? // Timers namespace rather than Threading ?? class SystemTimer { ??static void Main() { ????Timer tmr = new Timer();?????? // Doesn't require any args ????tmr.Interval = 500; ????tmr.Elapsed += tmr_Elapsed;??? // Uses an event instead of a delegate ????tmr.Start();?????????????????? // Start the timer ????Console.ReadLine(); ????tmr.Stop();??????????????????? // Pause the timer ????Console.ReadLine(); ????tmr.Start();?????????????????? // Resume the timer ????Console.ReadLine(); ????tmr.Dispose();???????????????? // Permanently stop the timer ??} ?? ??static void tmr_Elapsed (object sender, EventArgs e) { ????Console.WriteLine ("Tick"); ??} } |
????? .NET framework 還提供了第三個計時器——在System.Windows.Forms 命名空間下。雖然類似于System.Timers.Timer 的接口,但功能特性上有根本的不同。一個Windows Forms 計時器不能使用線程池,代替為總是在最初創建它的線程上觸發 "Tick"事件。假定這是主線程——負責實例化所有Windows Forms程序中的forms和控件,計時器的事件能夠操作forms和控件而不違反線程安全——或者強加單元線程模式。Control.Invoke是不需要的。它實質上是一個單線程timer
????? Windows Forms計時器必須迅速地執行來更新用戶接口。迅速地執行是非常重要的,因為Tick事件被主線程調用,如果它有停頓, 將使用戶接口變的沒有響應。
?
6. 局部存儲
?
????? 每個線程與其它線程數據存儲是隔離的,這對于“不相干的區域”的存儲是有益的,它支持執行路徑的基礎結構,如通信,事務和安全令牌。 通過方法參數傳遞這些數據是十分笨拙的。存儲這些數據到靜態域意味著這些數據可以被所有線程共享。
????? Thread.GetData從一個線程的隔離數據中讀,Thread.SetData 寫入數據。 兩個方法需要一個LocalDataStoreSlot對象來識別內存槽——這包裝自一個內存槽的名稱的字符串,這個名稱 你可以跨所有的線程使用,它們將得到不各自的值,看這個例子:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class ... { ????// 相同的LocalDataStoreSlot 對象可以用于跨所有線程 ??LocalDataStoreSlot secSlot = Thread.GetNamedDataSlot? ("securityLevel"); ????// 這個屬性每個線程有不同的值 ????int SecurityLevel { ????get { ????????object data = Thread.GetData (secSlot); ????????return data == null ? 0 : (int) data;??? // null == 未初始化 ???????} ????set { ????????Thread.SetData (secSlot, value); ????????} ??} ??... |
Thread.FreeNamedDataSlot將釋放給定的數據槽,它跨所有的線程——但只有一次,當所有相同名字LocalDataStoreSlot對象作為垃圾被回收時退出作用域時發生。這確保了線程不得到數據槽從它們的腳底下撤出——也保持了引用適當的使用之中的LocalDataStoreSlot對象。
轉載于:https://www.cnblogs.com/Van-Bumblebee/p/5490007.html
總結
以上是生活随笔為你收集整理的C#中的线程(三) 使用多线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Runtime.getRuntime()
- 下一篇: C#操作项目配置文件