線程同步的方式安全
線程同步有:臨界區、互斥區、事件、信號量四種方式 臨界區(Critical Section)、互斥量(Mutex)、信號量(Semaphore)、事件(Event)的區別
一、臨界區:經過對多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問。在任意時刻只容許一個線程對共享資源進行訪問,若是有多個線程試圖訪問公共資源,那麼在有一個線程進入後,其餘試圖訪問公共資源的線程將被掛起,並一直等到進入臨界區的線程離開,臨界區在被釋放後,其餘線程才能夠搶佔。
二、互斥量:採用互斥對象機制。 只有擁有互斥對象的線程纔有訪問公共資源的權限,由於互斥對象只有一個,因此能保證公共資源不會同時被多個線程訪問。互斥不只能實現同一應用程序的公共資源安全共享,還能實現不一樣應用程序的公共資源安全共享
三、信號量:它容許多個線程在同一時刻訪問同一資源,可是須要限制在同一時刻訪問此資源的最大線程數目 四、事 件: 經過通知操做的方式來保持線程的同步,還能夠方便實現對多個線程的優先級比較的操做多線程
C#中常見線程同步方法併發
一、Interlocked 爲多個線程共享的變量提供原子操做。dom
根據經驗,那些須要在多線程狀況下被保護的資源一般是整型值,且這些整型值在多線程下最多見的操做就是遞增、遞減或相加操做。Interlocked類提供了一個專門的機制用於完成這些特定的操做。這個類提供了Increment、Decrement、Add靜態方法用於對int或long型變量的遞增、遞減或相加操做。此類的方法能夠防止可能在下列狀況發生的錯誤:計劃程序在某個線程正在更新可由其餘線程訪問的變量時切換上下文;或者當兩個線程在不一樣的處理器上併發執行時。 此類的成員不引起異常。ide
Increment 和 Decrement 方法遞增或遞減變量並將結果值存儲在單個操做中。 在大多數計算機上,增長變量操做不是一個原子操做,須要執行下列步驟:函數
1)將實例變量中的值加載到寄存器中。 2)增長或減小該值。 3)在實例變量中存儲該值。 若是不使用 Increment 和 Decrement,線程會在執行完前兩個步驟後被搶先。 而後由另外一個線程執行全部三個步驟。 當第一個線程從新開始執行時,它覆蓋實例變量中的值,形成第二個線程執行增減操做的結果丟失。 測試
Exchange 方法自動交換指定變量的值。 CompareExchange 方法組合了兩個操做:比較兩個值以及根據比較的結果將第三個值存儲在其中一個變量中。 比較和交換操做按原子操做執行。ui
案例:打印機spa
class Program { static void Main(string[] args) { PrinterWithInterlockTest.TestPrint(); } } class PrinterWithInterlockTest { /// <summary> /// 正在使用的打印機 /// 0表明未使用,1表明正在使用 /// </summary> public static int UsingPrinter = 0; /// <summary> /// 計算機數量 /// </summary> public static readonly int ComputerCount = 3; /// <summary> /// 測試 /// </summary> public static void TestPrint() { Thread thread; Random random = new Random(); for (int i = 0; i < ComputerCount; i++) { thread = new Thread(MyThreadProc); thread.Name = string.Format("Thread{0}", i); Thread.Sleep(random.Next(3)); thread.Start(); } } /// <summary> /// 線程執行操做 /// </summary> private static void MyThreadProc() { //使用打印機進行打印 UsePrinter(); //當前線程等待1秒 Thread.Sleep(1000); } /// <summary> /// 使用打印機進行打印 /// </summary> private static bool UsePrinter() { //檢查打印機是否在使用,若是原始值爲0,則爲未使用,能夠進行打印,不然不能打印,繼續等待 if (0 == Interlocked.Exchange(ref UsingPrinter, 1)) { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //Code to access a resource that is not thread safe would go here. //Simulate some work Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); //釋放打印機 Interlocked.Exchange(ref UsingPrinter, 0); return true; } else { Console.WriteLine(" {0} was denied the lock", Thread.CurrentThread.Name); return false; } } }
二、lock 關鍵字操作系統
lock 關鍵字將語句塊標記爲臨界區,方法是獲取給定對象的互斥鎖,執行語句,而後釋放該鎖。 lock 確保當一個線程位於代碼的臨界區時,另外一個線程不進入臨界區。若是其餘線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。
public void Function() { System.Object locker= new System.Object(); lock(locker) { // Access thread-sensitive resources. } }
案例:繼續打印機
class Program { static void Main(string[] args) { PrinterWithInterlockTest.TestPrint(); } } class PrinterWithInterlockTest { private static object UsingPrinterLocker = new object(); /// <summary> /// 計算機數量 /// </summary> public static readonly int ComputerCount = 3; /// <summary> /// 測試 /// </summary> public static void TestPrint() { Thread thread; Random random = new Random(); for (int i = 0; i < ComputerCount; i++) { thread = new Thread(MyThreadProc); thread.Name = string.Format("Thread{0}", i); Thread.Sleep(random.Next(3)); thread.Start(); } } /// <summary> /// 線程執行操做 /// </summary> private static void MyThreadProc() { //使用打印機進行打印 UsePrinter(); //當前線程等待1秒 Thread.Sleep(1000); } /// <summary> /// 使用打印機進行打印 /// </summary> /// <summary> /// 使用打印機進行打印 /// </summary> private static void UsePrinter() { //臨界區 lock (UsingPrinterLocker) { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模擬打印操做 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } } }
結果:
Thread0 acquired the lock
Thread0 exiting lock
Thread1 acquired the lock
Thread1 exiting lock
Thread2 acquired the lock
Thread2 exiting lock
請按任意鍵繼續. . .
三、監視器
與 lock 關鍵字相似,監視器防止多個線程同時執行代碼塊。Enter 方法容許一個且僅一個線程繼續執行後面的語句;其餘全部線程都將被阻止,直到執行語句的線程調用 Exit。這與使用 lock 關鍵字同樣。事實上,lock 關鍵字就是用 Monitor 類來實現的。例如:(繼續修改共享打印機案例,增長方法UsePrinterWithMonitor)
/// <summary> /// 使用打印機進行打印 /// </summary> private static void UsePrinterWithMonitor() { System.Threading.Monitor.Enter(UsingPrinterLocker); try { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模擬打印操做 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } finally { System.Threading.Monitor.Exit(UsingPrinterLocker); } }
使用 lock 關鍵字一般比直接使用 Monitor 類更可取,一方面是由於 lock 更簡潔,另外一方面是由於 lock
確保了即便受保護的代碼引起異常,也能夠釋放基礎監視器。這是經過 finally 關鍵字來實現的,不管是否引起異常它都執行關聯的代碼塊。
四、同步事件和等待句柄
使用鎖或監視器對於防止同時執行區分線程的代碼塊頗有用,可是這些構造不容許一個線程向另外一個線程傳達事件。這須要「同步事件」,它是有兩個狀態(終止和非終止)的對象,能夠用來激活和掛起線程。讓線程等待非終止的同步事件能夠將線程掛起,將事件狀態更改成終止能夠將線程激活。若是線程試圖等待已經終止的事件,則線程將繼續執行,而不會延遲。
同步事件有兩種:AutoResetEvent 和 ManualResetEvent。它們之間惟一的不一樣在於,不管什麼時候,只要 AutoResetEvent 激活線程,它的狀態將自動從終止變爲非終止。相反,ManualResetEvent 容許它的終止狀態激活任意多個線程,只有當它的 Reset 方法被調用時才還原到非終止狀態。
等待句柄,能夠經過調用一種等待方法,如 WaitOne、WaitAny 或 WaitAll,讓線程等待事件。System.Threading.WaitHandle.WaitOne 使線程一直等待,直到單個事件變爲終止狀態;System.Threading.WaitHandle.WaitAny 阻止線程,直到一個或多個指示的事件變爲終止狀態;System.Threading.WaitHandle.WaitAll 阻止線程,直到全部指示的事件都變爲終止狀態。當調用事件的 Set 方法時,事件將變爲終止狀態。
AutoResetEvent 容許線程經過發信號互相通訊。 一般,當線程須要獨佔訪問資源時使用該類。線程經過調用 AutoResetEvent 上的 WaitOne 來等待信號。 若是 AutoResetEvent 爲非終止狀態,則線程會被阻止,並等待當前控制資源的線程經過調用 Set 來通知資源可用。調用 Set 向 AutoResetEvent 發信號以釋放等待線程。 AutoResetEvent 將保持終止狀態,直到一個正在等待的線程被釋放,而後自動返回非終止狀態。 若是沒有任何線程在等待,則狀態將無限期地保持爲終止狀態。若是當 AutoResetEvent 爲終止狀態時線程調用 WaitOne,則線程不會被阻止。 AutoResetEvent 將當即釋放線程並返回到非終止狀態。 能夠經過將一個布爾值傳遞給構造函數來控制 AutoResetEvent 的初始狀態:若是初始狀態爲終止狀態,則爲 true;不然爲 false。 AutoResetEvent 也能夠同 staticWaitAll 和 WaitAny 方法一塊兒使用。
案例:
class Example { private static AutoResetEvent event_1 = new AutoResetEvent(false); private static AutoResetEvent event_2 = new AutoResetEvent(false); static void Main() { Console.WriteLine("該示例將啓動三個線程\n" + "這些線程等待終止狀態中的已建立的 AutoResetEvent #1\n" + "分別等待按下enter來釋放線程\n" + "其中每一個線程執行完第一步都繼續等待AutoResetEvent #2\n" + "分別再次等待按下enter來釋放線程執行,完成後任何結束" ); Console.ReadLine(); for (int i = 1; i < 4; i++) { Thread t = new Thread(ThreadProc); t.Name = "Thread_" + i; t.Start(); } Thread.Sleep(250); for (int i = 0; i < 3; i++) { Console.WriteLine("Press Enter to release another thread."); Console.ReadLine(); event_1.Set(); Thread.Sleep(250); } Console.WriteLine("\r\nAll threads are now waiting on AutoResetEvent #2."); for (int i = 0; i < 3; i++) { Console.WriteLine("Press Enter to release a thread."); Console.ReadLine(); event_2.Set(); Thread.Sleep(250); } // Visual Studio: Uncomment the following line. //Console.Readline(); } static void ThreadProc() { string name = Thread.CurrentThread.Name; Console.WriteLine("{0} waits on AutoResetEvent #1.", name); event_1.WaitOne(); Console.WriteLine("{0} is released from AutoResetEvent #1.", name); Console.WriteLine("{0} waits on AutoResetEvent #2.", name); event_2.WaitOne(); Console.WriteLine("{0} is released from AutoResetEvent #2.", name); Console.WriteLine("{0} ends.", name); } }
結果:
五、Mutex對象
mutex 與監視器相似;它防止多個線程在某一時間同時執行某個代碼塊。事實上,名稱「mutex」是術語「互相排斥 (mutually exclusive)」的簡寫形式。然而與監視器不一樣的是,mutex 能夠用來使跨進程的線程同步。mutex 由 Mutex 類表示。當用於進程間同步時,mutex 稱爲「命名 mutex」,由於它將用於另外一個應用程序,所以它不能經過全局變量或靜態變量共享。必須給它指定一個名稱,才能使兩個應用程序訪問同一個 mutex 對象。 儘管 mutex 能夠用於進程內的線程同步,可是使用 Monitor 一般更爲可取,由於監視器是專門爲 .NET Framework 而設計的,於是它能夠更好地利用資源。相比之下,Mutex 類是 Win32 構造的包裝。儘管 mutex 比監視器更爲強大,可是相對於 Monitor 類,它所須要的互操做轉換更消耗計算資源。
本地 mutex 和系統 mutex Mutex 分兩種類型:本地 mutex 和命名系統 mutex。 若是使用接受名稱的構造函數建立了 Mutex 對象,那麼該對象將與具備該名稱的操做系統對象相關聯。 命名的系統 mutex 在整個操做系統中均可見,而且可用於同步進程活動。 您能夠建立多個 Mutex 對象來表示同一命名系統 mutex,並且您可使用 OpenExisting 方法打開現有的命名系統 mutex。 本地 mutex 僅存在於進程當中。 進程中引用本地 Mutex 對象的任意線程均可以使用本地 mutex。 每一個 Mutex 對象都是一個單獨的本地 mutex。
在本地Mutex中,用法與Monitor基本一致
繼續修改前面的打印機案例:
聲明Mutex對象:
/// <summary> /// mutex對象 /// </summary> private static Mutex mutex = new Mutex();
修改後的代碼:
class PrinterWithLockTest { /// <summary> /// 正在使用的打印機 /// </summary> private static object UsingPrinterLocker = new object(); /// <summary> /// 計算機數量 /// </summary> public static readonly int ComputerCount = 3; /// <summary> /// mutex對象 /// </summary> private static Mutex mutex = new Mutex(); /// <summary> /// 測試 /// </summary> public static void TestPrint() { Thread thread; Random random = new Random(); for (int i = 0; i < ComputerCount; i++) { thread = new Thread(MyThreadProc); thread.Name = string.Format("Thread{0}", i); Thread.Sleep(random.Next(3)); thread.Start(); } } /// <summary> /// 線程執行操做 /// </summary> private static void MyThreadProc() { //使用打印機進行打印 //UsePrinter(); //monitor同步 //UsePrinterWithMonitor(); //用Mutex同步 UsePrinterWithMutex(); //當前線程等待1秒 Thread.Sleep(1000); } /// <summary> /// 使用打印機進行打印 /// </summary> private static void UsePrinter() { //臨界區 lock (UsingPrinterLocker) { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模擬打印操做 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } } /// <summary> /// 使用打印機進行打印 /// </summary> private static void UsePrinterWithMonitor() { System.Threading.Monitor.Enter(UsingPrinterLocker); try { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模擬打印操做 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } finally { System.Threading.Monitor.Exit(UsingPrinterLocker); } } /// <summary> /// 使用打印機進行打印 /// </summary> private static void UsePrinterWithMutex() { mutex.WaitOne(); try { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模擬打印操做 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } finally { mutex.ReleaseMutex(); } } }
六、讀取器/編寫器
七、Semaphore 和 SemaphoreSlim
八、障礙(Barrier)4.0後技術
九、SpinLock(4.0後)
十、SpinWait(4.0後)
--後續補上