1.在多個線程的同步數據中,避免使用this、typeof(type)、string進行同步鎖,使用這3個容易造成死鎖。 2.使用Interlocked類:我們一般使用的互斥鎖定模式(同步數據)為Lock關鍵字(即Monitor類),這個同步屬於代價非常高的一種操作。除了使用Monitor之外,還有 ...
1.在多個線程的同步數據中,避免使用this、typeof(type)、string進行同步鎖,使用這3個容易造成死鎖。
2.使用Interlocked類:我們一般使用的互斥鎖定模式(同步數據)為Lock關鍵字(即Monitor類),這個同步屬於代價非常高的一種操作。除了使用Monitor之外,還有一個備選方案,它通常直接由處理器支持,而且面向特定的同步模式。Interlocked類中包含一些常用方法,如CompareExchange、Decrement、Increment、Exchange。這些都是針對單個的值(對象)進行同步數據處理。
3.多個線程時的事件通知:可以查看Utility.EventInThread()代碼清單。
4.同步設計的最佳實踐:
(1)避免死鎖;如兩個線程都等待對方鎖定的資源釋放,線程A鎖定sync1資源,線程B鎖定sync2資源,線程A請求鎖定sync2資源,線程B請求鎖定sync1資源,此時便出現死鎖。死鎖的發生必須滿足4個基本條件。互斥、占有並等待、不可搶先、迴圈等待條件。
(2)何時提供同步;通常針對靜態數據進行同步,並有公共方法來修改數據,方法內部應處理好同步問題。
(3)避免不必要的鎖定。
5.更多同步類型:System.Threading.Mutex類在概念上與Monitor類一致,只是其是為支持進程之間的同步。如同步對文件或其他跨進程資源的訪問,限製程序只能運行一個實例。如Utility.UseMutex()代碼清單。Mutex類派生自WaitHandle,可以自動獲取多個鎖(這是Monitor類所不支持的)。
6.WaitHandle類:多個同步類是繼承於它,如Mutex、EventWaitHandle、Semaphore,WaitHandle類的關鍵方法為WaitOne(),它有多個重載版本,這些方法會阻塞當前線程,直到WaitHandle實例收到信號或被設置(調用Set())。
7.重置事件類:重置事件與C#中委托以及事件沒有任何關係,用於多線程的控制,重置事件用於強迫代碼等候另一個線程的執行,直到獲得事件已發生的通知。重置事件類有ManualResetEvent、ManualResetEventSlim(.net4.0新增,針對前者進行優化)、AutoResetEvent(主要使用前面2個類型),它們提供的關鍵方法為Set()與Wait()。調用Wait()方法會阻塞一個線程的執行,直到一個不同的線程調用Set(),或者設定的等待時間結束,才會繼續運行。可查看Utility.UseManualResetEvent()代碼清單。
8.併發集合類:.net4.0新增了一些類是併發集合類,這些類專門設計用來包含內建的同步代碼,使它們能支持多個線程訪問而不必關心競態條件。如BlockingCollection<T>、ConcurrentBag<T>、ConcurrentDictionary<K,V>、ConcurrentQueue<T>、ConcurrentStack<T>,利用併發集合,可以實現的一個常見的模式是生產者和消費者的線程安全的訪問。
9.線程本地存儲:同步的一個替代方案是隔離,而實現隔離的一個辦法是使用線程本地存儲,利用線程本地存儲,線程就有了專屬的變數實例。線程本地存儲實現有2中方式,分別為ThreadLocal<T>和ThreadStaticAttribute類,其中ThreadLocal<T>類是.net4.0新增的。可查看LocalVarThread類的代碼清單。
10.計時器:有三種計時器分別為System.Windows.Forms.Timer、System.Timers.Timer、System.Threading.Timer,Forms.Timer用於用戶界面編程,能夠安全的訪問用戶界面上的窗體與控制項,Timers.Timer是Threading.Timer的包裝器,是對其功能的抽象(System.Threading.Timer類型輕量一些)。
功能描述 |
System.Timers.Timer |
System.Threading.Timer |
System.Window.Forms.Timer |
支持在計時器實例化之後添加和刪除偵聽器 |
是 |
否 |
是 |
支持用戶界麵線程上的回調 |
是 |
否 |
是 |
從自線程池獲取的線程進行回調 |
是 |
是 |
否 |
支持在Windows窗體設計器中拖放 |
是 |
否 |
是 |
適合在一個多線程伺服器環境中運行 |
是 |
是 |
否 |
支持將任意狀態從計時器初始化傳遞至回調 |
否 |
是 |
否 |
實現Idisposable |
是 |
是 |
是 |
支持開關式回調和定期重覆回調 |
是 |
是 |
是 |
可穿越應用程式域的邊界訪問 |
是 |
是 |
是 |
支持Icomponent,可容納在一個Icontainer中 |
是 |
否 |
是 |
11.非同步編程模型(Async Program Model,APM):非同步編程是多線程的一種方式,APM的關鍵在於成對使用BeginX和EndX方法(X一般對應同步版本的方法名),而且這些方法具有完善的簽名。BeginX返回一個System.IAsyncResult對象,可通過它訪問非同步調用的狀態,以便等待完成或輪詢完成。然而EndX方法獲取這個返回的對象作為輸入參數。這樣才真正將兩個方法配成一對,讓我們可以清晰地判斷哪個BeginX方法調用和哪個EndX方法調用配對。APM的本質要求所有BeginX調用都必須有一個(而且只能有一個)EndX調用。因此,不可能發生兩個EndX調用接受同一個IAsyncResult實例的情況。我們還可以使用IAsyncResult的WaitHandle判斷非同步方法何時結束,IAsyncResult的WaitHandle是在回調執行之前進行通知完成。
EndX方法具有4個方面的用途。
首先,調用EndX會阻塞線程繼續執行,直到請求的工作成功完成(或者發生錯誤並引發異常)。
其次,如果方法X要返回數據,這個數據可從EndX方法調用中訪問。
再次,如果執行請求的工作時發生異常,可在調用EndX時重新引發這個異常,確保異常會被調用代碼發現——好像它是在一次同步調用上發生的那樣。
最後,如果任何資源需要在調用X後清理,EndX將負責清理這些資源。
BeginX方法有兩個額外的參數,在同步版本的方法中是沒有的,一個是回調參數,是方法結束時要調用的一個System.AsyncCallback委托,另一個是object類型的狀態參數(State)。在使用回調時,可以把EndX放在其內部執行。
12.使用TPL(任務並行庫)調用APM:雖然TPL大幅簡化了長時間運行方法的非同步調用,但通常最好是使用API提供的APM方法,而不是針對同步版本編寫TPL。這是因為API開發人員知道如何編寫最高效率的線程處理代碼,知道同步哪些數據以及要使用什麼同步類型。TPL包含FromAsync的一組重載版本,用於調用APM。
13.非同步委托調用:有一個派生的APM模式,稱為非同步委托調用,它在所有委托數據類型上使用了特殊的、由C#編譯器生成的代碼。例如,給定Func<string,int>的一個委托實例,可以在這個實例上使用以下APM方法對。
System.IAsyncResult BeginInvoke(string arg,AsyncCallback callback,object obj)
Int EndInvode(IasyncResult result)
結果是可以使用C#編譯器生成的方法來同步地調用任何委托(進而調用任何方法)。遺憾的是,非同步委托調用模式使用的基礎技術是一種不再繼續開發的分散式編程技術,稱為遠程處理。雖然微軟仍然支持非同步委托調用,而且在可以預見的將來,也不會放棄對它的支持,但和其他技術相比,它的性能顯得比較一般。其他技術包括Thread、ThreadPool和TPL等。因此,在開發新項目時,開發人員應儘量選用其他技術,而不要使用非同步委托調用API。在TPL之前,非同步委托調用模式比其他替代方案容易得多,所以假如一個API沒有提供顯式的非同步調用模式,一般都會選用它。然而,在TPL問世之後,除非是為了與.Net3.5和早期框架版本相容,否則非同步委托調用越來越沒有什麼用了。
14.基於事件的非同步模式(EAP):比APM更高級的一種編程模式是基於事件的非同步模式。和APM一樣,API開發人員為長時間運行的方法實現了EAP。其中Background Worker模式,它是EAP的一個特定的實現。
15.Background Worker模式:建立Background Worker模式的過程如下。
(1)為BackgroundWorker.DoWork事件註冊長時間運行的方法。
(2)為了接受進展或狀態通知,要為BackgroundWorker.ProgressChanged掛接一個偵聽器,並將BackgroundWorker.WorkerReportsProgress設為true。
(3)為BackgroundWorker.RunWorkerCompleted事件註冊一個方法。
(4)為WorkerSupportsCancellation屬性賦值以支持取消一個操作。將true值賦給該屬性以後,對BackgroundWorker.CancelAsync的調用就會設置DoWorkEventArgs.CancellationPending標誌。
(5)在DoWork提供的方法內,檢查DoWorkEventArgs.CancellationPending屬性值,併在它為true時退出方法。
(6)一切都設置好之後,調用BackgroundWorker.RunWorkerAsync(),並提供要傳給指定DoWork()方法的一個狀態參數來開始工作。
分解成以上小步驟以後,Background Worker模式就顯得容易理解。另外,由於它本質上是一種EAP,所以提供了對進度通知的顯式支持。後臺的工作者(worker)線程非同步執行的時候,假如發生一個未處理的異常,RunWorkerCompleted委托的RunWorkerCompletedEventArgs.Error屬性就會設置成Exception實例。因此,我們通過在RunWorkerCompleted回調內檢查Error屬性來提供異常處理機制。
16.Windows UI編程:使用System.Windows.Forms和System.Windows命名空間來進行用戶界面開發時,也必須註意線程處理問題。Microsoft Windows系列操作系統使用的是一個單線程的、基於消息處理的用戶界面。這意味著,每次只能有一個線程訪問用戶界面,與輪換線程的任何交互都應該通過Windows消息泵來封送。
17.Windows窗體:進行Windows窗體編程時,為了檢查是否允許從一個線程中發出UI調用,需要調用一個組件的InvokeRequired屬性,判斷是否需要進行封送處理。如果InvokeRequired返回true,表明需要封送,並可通過一個Invoke()調用來實現。儘管Invoke()在內部無論如何都會檢查InvokeRequired,但更有效率的做法是提前顯示地檢查這個屬性。由於封送到另一個線程可能是相當慢的一個過程,所以可以通過BeginInvoke()和EndInvoke()來進行非同步調用。Invoke()、BeginInvoke()、EndInvoke()和InvokeRequired構成了System.ComponentModel.ISynchronizeInvoke介面的成員。該介面已由System.Windows.Forms.Control實現,所有Windows窗體控制項都是從這個Control類派生。
18.Windows Presentation Foundation(WPF):為了在WPF平臺上實現相同的封送檢查,需要採取稍有不同的一種方式。WPF包含一個名為Current的靜態成員屬性,它的類型是DispatcherObject,由System.Windows.Application類提供。在調度器(dispatcher)對象上調用CheckAccess(),作用等同於在Windows窗體中的控制項上調用InvokeRequired,然後再使用Application.Current.Dispatcher.Invoke()方法封送。
19.說明:除了TPL提供的模式,還有這麼多額外的模式可供選用,這造成許多人不知道應該如何選擇。一般情況下,最好是選擇由API提供的模式(比如APM或EAP),最後選擇TPL模式。
public class Utility { private static ManualResetEventSlim firstEvent, secendEvent; private static object _data; public static void Initialize(object newValue) { Interlocked.CompareExchange(ref _data, newValue, null); } public static void EventInThread() { //不是線程安全,在檢查委托對象與調用委托之間,存在其他線程對OnTemparatureChange進行賦值操作,可能會被設置為null。 /*if (OnTemparatureChange != null) { //調用訂閱者 OnTemparatureChange(this, new TemparatureEventArgs()); }*/ //線程安全操作,創建一個委托變數副本,檢查副本的null,再觸發副本。這樣即使OnTemparatureChange委托變數在其他線程中被null化,也不影響。 /*TemparatureChangedHandle localChanged = OnTemparatureChange; if (localChanged != null) { //調用訂閱者 localChanged(this, new TemparatureEventArgs()); }*/ } public static void UseMutex() { bool firstApplicationInstance; string mutexName = Assembly.GetEntryAssembly().FullName; using (Mutex mutex = new Mutex(false, mutexName, out firstApplicationInstance)) { if (!firstApplicationInstance) { Console.WriteLine("This Application is already running."); } else { Console.WriteLine("Enter to shutdown."); Console.ReadLine(); } } } public static void UseManualResetEvent() { using (firstEvent = new ManualResetEventSlim()) using (secendEvent = new ManualResetEventSlim()) { Console.WriteLine("App start"); Console.WriteLine("start task"); Task task = Task.Factory.StartNew(() => { Console.WriteLine("DoWork start"); Thread.Sleep(1000); firstEvent.Set(); secendEvent.Wait(); Console.WriteLine("DoWork end"); }); firstEvent.Wait(); Console.WriteLine("Thread executing"); secendEvent.Set(); task.Wait(); Console.WriteLine("Thread completed"); Console.WriteLine("App shutdown"); } } } public class LocalVarThread { public static ThreadLocal<double> _count = new ThreadLocal<double>(() => 0.01134); public static double Count { set { _count.Value = value; } get { return _count.Value; } } public static void DoWork() { Task.Factory.StartNew(Decrement); for (int i = 0; i < short.MaxValue; i++) { Count++; } Console.WriteLine("DoWork Count = {0}", Count); } public static void Decrement() { Count = -Count; for (int i = 0; i < short.MaxValue; i++) { Count--; } Console.WriteLine("Decrement Count = {0}", Count); } }View Code
---------------------以上內容根據《C#本質論 第三版》進行整理