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