最初學習多線程的時候,只學了用Thread這個類,記憶中也用過Mutex,到后來只記得Thread的使用,其余的都忘了。知道前不久寫那個Socket連接池時遇到了一些對象如:Semaphore,Interlocked,Mutex等,才知道多線程中有這么多好東西,當時用了一下有初步了解,現在來熟悉熟悉。
本文介紹的多線程這個“象群”包括:Interlocked,Semaphore,Mutex,Monitor,ManualResetEvent,AutoRestEvent。而使用的例子則有車票競搶和類似生產者消費者的Begin/End(這里的Begin/End跟異步里面的沒關系)兩個事件模型。
先來看一下本文“象群”的類圖
Interlocked(為多個線程共享的變量提供原子操作)
在平常多線程中為了保護某個互斥的資源在多線程中不會因為資源共享而出問題,都會使用lock關鍵字。如果這個資源只是一個單單的計數量的話,就可以用這個Interlocked了,調用Increment方法可以是遞增,Decrement則是遞減。下面則是MSDN上的說明
此類的方法可以防止可能在下列情況發生的錯誤:計划程序在某個線程正在更新可由其他線程訪問的變量時切換上下文;或者當兩個線程在不同的處理器上並發執行時。 此類的成員不引發異常。
由於這里就使用車票競搶的例子吧!假設有10張車票,有多個售票點去銷售,賣光就沒有了
這個是線程的方法
1 public void ThreadingCount2() 2 { 3 while (true) 4 { 5 //賣光就停止銷售了 6 if (count >= 10) 7 break; 8 Interlocked.Increment(ref count); 9 //搶到車票的要幫上售票點和座位號 10 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " : " + count); 11 //為了防止機子的性能太好,資源都都給一個線程搶光了,就休眠一段時間 12 Thread.Sleep(500); 13 } 14 }
這里開三個線程,模擬三個售票點去賣這10張票
1 private void MutexTest() 2 { 3 count = 0; 4 Thread t1 = new Thread(ThreadingCount2); 5 Thread t2 = new Thread(ThreadingCount2); 6 Thread t3 = new Thread(ThreadingCount2); 7 t1.Start(); 8 t2.Start(); 9 t3.Start(); 10 }
運行結果
Semaphore (限制可同時訪問某一資源或資源池的線程數)
這個稱之為信號量,也有些人叫它作信號燈。這個概念倒是在操作系統中聽過,現在用起來就感覺可以通過信號量來限制進入某段區域的次數,通過調用WaitOne和Release方法,這個挺適合生產者與消費者那個問題的。記得解決生產者與消費者的問題上有用到這個信號量。下面則是MSDN的說明:
使用 Semaphore 類可控制對資源池的訪問。 線程通過調用 WaitOne 方法(從 WaitHandle 類繼承)進入信號量,並通過調用 Release 方法釋放信號量。
信號量的計數在每次線程進入信號量時減小,在線程釋放信號量時增加。 當計數為零時,后面的請求將被阻塞,直到有其他線程釋放信號量。 當所有的線程都已釋放信號量時,計數達到創建信號量時所指定的最大值。
被阻止的線程並不一定按特定的順序(如 FIFO 或 LIFO)進入信號量。
下面則用Begin/End模型來作為例子,它這不停地交替輸出Begin和End,每輸出一次Begin,就會暫停,直到輸出了一次End,才會輸出下一個Begin。用兩個線程,一個是專門輸出Begin的;另一個是輸出End的。
Begin的線程方法如下
1 private void Begin() 2 { 3 while (true) 4 { 5 semaphore.WaitOne(); 6 Console.WriteLine(Thread.CurrentThread.ManagedThreadId+ " Begin"); 7 } 8 }
這里先是等待信號才去輸出,這個輸出就相當於進行某一些操作了,如果把Waitone放到輸出的后面,就限制不了對某個操作進行次數限制。當然,這樣做的話,對semaphore對象構造時也會不同。
End的線程方法如下
1 private void End() 2 { 3 while (true) 4 { 5 Thread.Sleep(1000); 6 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " End"); 7 semaphore.Release(); 8 } 9 }
這里休眠1秒作用有兩個,第一是等待Begin先運行才釋放信號,第二是控制輸出的節奏,免得屏幕上猛的刷一大堆Begin/End,看不清什么東西了。
Semaphore構造時是這樣的semaphore = new Semaphore(1, 1);第一個參數是初始化時的信號量,第二個參數是總的信號量,調用則是這樣,兩個線程輸出Begin,一個線程數據End
1 Thread t1 = new Thread(Begin); 2 Thread t3 = new Thread(Begin); 3 Thread t2 = new Thread(End); 4 t1.Start(); 5 t3.Start(); 6 t2.Start();
運行的結果,兩個線程會搶着輸出Begin,輸出了Begin之后就會被阻塞,等到End輸出了之后才能進行下一次爭奪Begin的輸出
有位園友說,我老是用那個Sleep方法不好,於是這里就給一個沒有用Sleep方法的Begin/End版本。
用到的信號量就要兩個了,一個是用於阻塞Begin的,一個是用於阻塞End的,初始時值也有出入。End的要讓它先阻塞,Begin的要讓它先通過
1 private Semaphore semBegin, semEnd; 2 semBegin = new Semaphore(1, 1); 3 semEnd = new Semaphore(0, 1);
1 private void Begin() 2 { 3 for (int i = 0; i < 5; i++) 4 { 5 semBegin.WaitOne(); 6 Console.WriteLine(Thread.CurrentThread.ManagedThreadId+" : Begin "); 7 semEnd.Release(); 8 } 9 } 10 11 private void End() 12 { 13 while (true) 14 { 15 semEnd.WaitOne(); 16 Console.WriteLine(Thread.CurrentThread.ManagedThreadId+" : End "); 17 semBegin.Release(); 18 } 19 } 20 }
這樣使用信號量有死鎖的嫌疑,但是實踐過是沒有的。運行結果與之前的一樣,暫時不考慮信號量的關閉與線程關閉等問題。
Mutex (一個同步基元,也可用於進程間同步)
這個稱之為互斥體。這個互斥體跟lock關鍵字差不多,是保證某片代碼區域只能給一個線程訪問,通過調用WaitOne來掛起線程等待信號和ReleaseMutex釋放一次互斥信號來喚醒當前線程這樣的方式來實現。這個掛起只會掛起后來進入這片區域的線程,最初的線程在喚醒之前無論遇到多少個WaitOne照樣過,不過在之前WaitOne了多少次,到后來就要相應釋放那么多次,否則別的線程一直被掛起到某個WaitOne處,雖然把等待和釋放分開了兩個方法,但放在不同線程去調用的話只會拋異常,因為這兩個方法要在一個同步的區域內調用的。下面則是MSDN的說明。
當兩個或更多線程需要同時訪問一個共享資源時,系統需要使用同步機制來確保一次只有一個線程使用該資源。 Mutex 是同步基元,它只向一個線程授予對共享資源的獨占訪問權。 如果一個線程獲取了互斥體,則要獲取該互斥體的第二個線程將被掛起,直到第一個線程釋放該互斥體。
既然這個互斥體的用法跟lock那么相像,我用搶車票的例子吧!這里變的只是線程的方法而已,創建線程的跟原來的一樣,不再重復粘貼了
1 private void ThreadingCount() 2 { 3 while (true) 4 { 5 mutex.WaitOne(); 6 if (count > 10) 7 { 8 mutex.ReleaseMutex(); 9 break; 10 } 11 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " : " + count++); 12 mutex.ReleaseMutex(); 13 Thread.Sleep(500); 14 } 15 }
構造對象時這樣mutex = new Mutex();,運行結果如下
ManualResetEvent(通知一個或多個正在等待的線程已發生事件)與AutoResetEvent (通知正在等待的線程已發生事件)
這兩個類很相似,都是調用了WaitOne就阻塞當前線程等待信號,直到調用了Set才發了信號喚醒阻塞的線程。不同點就在調用Set方法之后了,AutoResetEvent 只是喚醒一個線程,但是就喚醒了所有等待信號而阻塞的線程,並且需要調用Reset關閉了信號,才能使WaitOne處能阻塞線程。下面分別是MSDN上對它們的描述
ManualResetEvent 使線程可以通過發信號來互相通信。 通常,此通信涉及一個線程在其他線程進行之前必須完成的任務。
AutoResetEvent 使線程可以通過發信號來互相通信。 通常,此通信涉及線程需要獨占訪問的資源。
這里就用Begin/End的作例子
兩個類用起來基本一樣,就效果一樣而已,出於篇幅的考慮,只上一次代碼算了
1 private void Begin() 2 { 3 while (true) 4 { 5 //等待信號 6 //autoreset.WaitOne(); 7 manualreset.WaitOne(); 8 Console.WriteLine(Thread.CurrentThread.ManagedThreadId+ " Begin"); 9 //關閉信號 10 manualreset.Reset(); 11 //這里對於autorest來說其實可以需要 12 //因為調用Set()之后就會關閉信號了 13 //autoreset.Reset(); 14 } 15 } 16 17 private void End() 18 { 19 while (true) 20 { 21 Thread.Sleep(1000); 22 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " End"); 23 //semaphore.Release(); 24 manualreset.Set(); 25 //autoreset.Set(); 26 } 27 }
阻塞線程和輸出Begin的道理和前面使用Semaphore 的一樣,都是為了確保能互斥地執行那個操作,可是對於使用ManualResetEvent 就不是這樣說了,看看結果就知道了
這個是ManualResetEvent 的運行結果,一發出了信號,之前等待信號的兩個線程都同時被喚醒了,一齊去輸出Begin,兩個線程又在關閉信號之后阻塞在等待信號的地方。
而AutoResetEvent 的結果則不同,Begin和End都是一個挨着一個交替輸出,那個線程搶到了信號就能輸出Begin,搶不到的就一直阻塞在那里。
對了,兩個對象的構造如下
manualreset = new ManualResetEvent(true); autoreset = new AutoResetEvent(true);
true是初始狀態,true就一開始有信號,免得沒信號就一直卡在那里,要等End執行了才放行,這樣有了End才有Begin就不對了。
這里也同樣給出不用Sleep的版本,同樣所需要的對象也比原本的多了
1 private ManualResetEvent manBegin, manEnd; 2 3 private AutoResetEvent autoBegin, autoEnd; 4 5 manBegin = new ManualResetEvent(true); 6 manEnd = new ManualResetEvent(false); 7 8 autoBegin = new AutoResetEvent(true); 9 autoEnd = new AutoResetEvent(false);
初始狀態跟上面使用信號量的道理一樣。
1 private void Begin() 2 { 3 for (int i = 0; i < 5; i++) 4 { 5 manBegin.WaitOne(); 6 //manBegin.Reset();//在這里Reset就只能是一個Begin一個End 7 //autoBegin.WaitOne(); 8 Console.WriteLine(Thread.CurrentThread.ManagedThreadId+" : Begin "); 9 manBegin.Reset();//在這里Reset就兩個Begin一個End 10 manEnd.Set(); 11 //autoEnd.Set(); 12 } 13 } 14 15 private void End() 16 { 17 while (true) 18 { 19 manEnd.WaitOne(); 20 manEnd.Reset(); 21 //autoEnd.WaitOne(); 22 Console.WriteLine(Thread.CurrentThread.ManagedThreadId+" : End "); 23 manBegin.Set(); 24 //autoBegin.Set(); 25 } 26 } 27 }
這里使用ManualResetEvent類的時候有兩種情況,注釋中有說明,關閉信號的地方不同,會影響到Begin輸出的數量,在這里也用ManualResetEvent類實現Begin和End間隔輸出。
Monitor(提供同步訪問對象的機制)
這個類是在網上看別人的博文時看到的,這個類比較原始。還是先看看MSDN的說明吧!
Monitor類通過向單個線程授予對象鎖來控制對對象的訪問。 對象鎖提供限制訪問代碼塊(通常稱為臨界區)的能力。 當一個線程擁有對象的鎖時,其他任何線程都不能獲取該鎖。 還可以使用 Monitor 來確保不會允許其他任何線程訪問正在由鎖的所有者執行的應用程序代碼節,除非另一個線程正在使用其他的鎖定對象執行該代碼。
Enter方法和Exit方法已經被封裝成lock關鍵字了。這里也給個使用Enter和Exit方法的例子,搶票問題的
1 private void ThreadingCount() 2 { 3 while (true) 4 { 5 Monitor.Enter(objFlag); 6 if (count > 10) 7 { 8 Monitor.Exit(objFlag); 9 break; 10 } 11 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " : " + count++); 12 Thread.Sleep(500); 13 Monitor.Exit(objFlag); 14 } 15 }
Enter和Exit方法都要傳一個object類型的參數,作用就跟lock的鎖旗標一樣。
Monitor除了能實現搶票這類的問題外,同樣也能解決Begin/End的問題的。它有個Wait和Pluse方法。下面則列舉出另一個例子的代碼
1 private void Begin() 2 { 3 lock (objFlag) 4 { 5 Monitor.Pulse(objFlag); 6 } 7 while (true) 8 { 9 lock (objFlag) 10 { 11 //調用Wait方法釋放對象上的鎖並阻止該線程(線程狀態為WaitSleepJoin) 12 //該線程進入到同步對象的等待隊列,直到其它線程調用Pulse使該線程進入到就緒隊列中 13 //線程進入到就緒隊列中才有條件爭奪同步對象的所有權 14 //如果沒有其它線程調用Pulse/PulseAll方法,該線程不可能被執行 15 Monitor.Wait(objFlag); 16 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Begin"); 17 } 18 } 19 } 20 21 private void End() 22 { 23 Thread.Sleep(1000); 24 while (true) 25 { 26 lock (objFlag) 27 { 28 //通知等待隊列中的線程鎖定對象狀態的更改,但不會釋放鎖 29 //接收到Pulse脈沖后,線程從同步對象的等待隊列移動到就緒隊列中 30 //注意:最終能獲得鎖的線程並不一定是得到Pulse脈沖的線程 31 Monitor.Pulse(objFlag); 32 33 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " End"); 34 //釋放對象上的鎖並阻止當前線程,直到它重新獲取該鎖 35 //如果指定的超時間隔已過,則線程進入就緒隊列 36 Monitor.Wait(objFlag, 1000); 37 } 38 } 39 } 40 }
當然這個例子其實挺生搬硬套的,為了讓Begin先輸出,就Pluse一次,同時又讓End的線程休眠。如果Begin的線程不運行,End的照樣能正常輸出,這里希望各位有什么高見的不要吝嗇,盡管提出來。下面是運行結果。
上面如果有什么不足的或遺漏的或說錯的,請各位盡情指出。謝謝!