C# 溫故而知新: 線(xiàn)程篇(三) 線(xiàn)程同步篇 (上)
在多線(xiàn)程的環(huán)境中,,可能需要共同使用一些公共資源,這些資源可能是變量,,方法邏輯段等等,,這些被多個(gè)線(xiàn)程 共用的區(qū)域統(tǒng)稱(chēng)為臨界區(qū)(共享區(qū)),聰明的你肯定會(huì)想到,,臨界區(qū)的資源不是很安全,,因?yàn)榫€(xiàn)程的狀態(tài)是不定的,所以 可能帶來(lái)的結(jié)果是臨界區(qū)的資源遭到其他線(xiàn)程的破壞,,我們必須采取策略或者措施讓共享區(qū)數(shù)據(jù)在多線(xiàn)程的環(huán)境下保持 完成性不讓其受到多線(xiàn)程訪(fǎng)問(wèn)的破壞
可能大家覺(jué)得這個(gè)很難理解,,的確如果光看概念解釋的話(huà),,會(huì)讓人抓狂的,,因?yàn)檫@個(gè)模式牽涉到了深?yuàn)W的底層cup 內(nèi)核和windows的一些底層機(jī)制,所以我用最簡(jiǎn)單的理解相信大家一定能理解,,因?yàn)檫@對(duì)于理解同步也很重要 回到正題,,基元用戶(hù)模式是指使用cpu的特殊指令來(lái)調(diào)度線(xiàn)程,所以這種協(xié)調(diào)調(diào)度線(xiàn)程是在硬件中進(jìn)行的所以得出 了它第一些優(yōu)點(diǎn): 速度特別快 線(xiàn)程阻塞時(shí)間特別短 但是由于該模式中的線(xiàn)程可能被系統(tǒng)搶占,,導(dǎo)致該模式中的線(xiàn)程為了獲取某個(gè)資源,,而浪費(fèi)許多cpu時(shí)間,同時(shí)如果一直處 于等待的話(huà)會(huì)導(dǎo)致”活鎖”,,也就是既浪費(fèi)了內(nèi)存,,又浪費(fèi)了cpu時(shí)間,這比下文中的死鎖更可怕,,那么如何利用強(qiáng)大的 cpu時(shí)間做更多的事呢,?那就引出了下面的一個(gè)模式
該模式和用戶(hù)模式不同,它是windows系統(tǒng)自身提供的,,使用了操作系統(tǒng)中內(nèi)核函數(shù),,所以它能夠阻塞線(xiàn)程提高了cpu的利 用率,,同時(shí)也帶來(lái)了一個(gè)很可怕的bug,死鎖,可能線(xiàn)程會(huì)一直阻塞導(dǎo)致程序的奔潰,常用的內(nèi)核模式的技術(shù)例如Monitor,Mutex, 等等會(huì)在下一章節(jié)介紹,。本章將詳細(xì)討論鎖的概念,,使用方法和注意事項(xiàng)
如果一個(gè)語(yǔ)句執(zhí)行一個(gè)單獨(dú)不可分割的指令,那么它是原子的,。嚴(yán)格的原子操作排除了任何搶占的可能性(這也是實(shí)現(xiàn)同步的一 個(gè)重要條件,,也就是說(shuō)沒(méi)有一個(gè)線(xiàn)程可以把這個(gè)美女占為己有,更方便的理解是這個(gè)值永遠(yuǎn)是最新的),,在c#中原子操作如下圖所示: 其實(shí)要符合原子操作必須滿(mǎn)足以下條件
相信大家能夠理解原子的特點(diǎn),,下文中的Volatil和interlocked會(huì)詳細(xì)模擬原子操作來(lái)實(shí)現(xiàn)線(xiàn)程同步,,所以在使用原子操 作時(shí)也需要注意當(dāng)前操作系統(tǒng)是32位或是64位cpu或者兩者皆要考慮
非阻止同步說(shuō)到底,就是利用原子性操作實(shí)現(xiàn)線(xiàn)程間的同步,,不刻意阻塞線(xiàn)程,,減少相應(yīng)線(xiàn)程的開(kāi)銷(xiāo),下文中的VolatileRead,V olatileWrite,Volatile關(guān)鍵字,,interlocked類(lèi)便是c#中非阻止同步的理念所產(chǎn)生的線(xiàn)程同步技術(shù)
阻止同步正好相反,,其實(shí)阻止同步也是基元內(nèi)核模式的特點(diǎn)之一,例如c# 中的鎖機(jī)制,,及其下幾章介紹的mutex,monitor等都屬 于阻止同步,,他們的根本目的是,以互斥的效果讓同一時(shí)間只有一個(gè)線(xiàn)程能夠訪(fǎng)問(wèn)共享區(qū),,其他線(xiàn)程必須阻止等待,,直到該線(xiàn)程離開(kāi)共享 區(qū)后,在讓其他一個(gè)線(xiàn)程訪(fǎng)問(wèn)共享區(qū),,阻止同步缺點(diǎn)也是容易產(chǎn)生死鎖,,但是阻止同步提高了cpu時(shí)間的利用率
2.詳解Thread類(lèi)中的VolatileRead和VolatileWrite方法和Volatile關(guān)鍵字 前文中,我們已經(jīng)對(duì)原子操作和非阻止同步的概念已經(jīng)有了大概的認(rèn)識(shí),,接著讓我們從新回到Thread類(lèi)來(lái)看下其中比較經(jīng)典的VolatileRead 和VolatileWrite方法
可能這樣的解釋會(huì)讓大家困惑,老規(guī)矩,,直接上例子讓大家能夠理解: /// <summary> /// 本例利用VolatileWrite和VolatileRead來(lái)實(shí)現(xiàn)同步,,來(lái)實(shí)現(xiàn)一個(gè)計(jì)算 /// 的例子,每個(gè)線(xiàn)程負(fù)責(zé)運(yùn)算1000萬(wàn)個(gè)數(shù)據(jù),,共開(kāi)啟10個(gè)線(xiàn)程計(jì)算至1億,, /// 而且每個(gè)線(xiàn)程都無(wú)法干擾其他線(xiàn)程工作 /// </summary> class Program { static Int32 count;//計(jì)數(shù)值,用于線(xiàn)程同步 (注意原子性,,所以本例中使用int32) static Int32 value;//實(shí)際運(yùn)算值,,用于顯示計(jì)算結(jié)果 static void Main(string[] args) { //開(kāi)辟一個(gè)線(xiàn)程專(zhuān)門(mén)負(fù)責(zé)讀value的值,這樣就能看見(jiàn)一個(gè)計(jì)算的過(guò)程 Thread thread2 = new Thread(new ThreadStart(Read)); thread2.Start(); //開(kāi)辟10個(gè)線(xiàn)程來(lái)負(fù)責(zé)計(jì)算,,每個(gè)線(xiàn)程負(fù)責(zé)1000萬(wàn)條數(shù)據(jù) for (int i = 0; i < 10; i++) { Thread.Sleep(20); Thread thread = new Thread(new ThreadStart(Write)); thread.Start(); } Console.ReadKey(); } /// <summary> /// 實(shí)際運(yùn)算寫(xiě)操作 /// </summary> private static void Write() { Int32 temp = 0; for (int i = 0; i < 10000000; i++) { temp += 1; } value += temp; //注意VolatileWrite 在每個(gè)線(xiàn)程計(jì)算完畢時(shí)會(huì)寫(xiě)入同步計(jì)數(shù)值為1,,告訴程序該線(xiàn)程已經(jīng)執(zhí)行完畢 //所以VolatileWrite方法類(lèi)似與一個(gè)按鈴,往往在原子性的最后寫(xiě)入告訴程序我完成了 Thread.VolatileWrite(ref count, 1); } /// <summary> /// 顯示計(jì)算后的數(shù)據(jù),,使用該方法的線(xiàn)程會(huì)死循環(huán)等待寫(xiě) /// 操作的線(xiàn)程發(fā)出完畢信號(hào)后顯示當(dāng)前計(jì)算結(jié)果 /// </summary> private static void Read() { while (true) { //一旦監(jiān)聽(tīng)到一個(gè)寫(xiě)操作線(xiàn)執(zhí)行完畢后立刻顯示操作結(jié)果 //和VolatileWrite相反,,VolatileRead類(lèi)似一個(gè)門(mén)禁,只有原子性的最先讀取他,,才能達(dá)到同步效果 //同時(shí)count值保持最新 if (Thread.VolatileRead(ref count) > 0) { Console.WriteLine("累計(jì)計(jì)數(shù):{1}", Thread.CurrentThread.ManagedThreadId, value); //將count設(shè)置成0,,等待另一個(gè)線(xiàn)程執(zhí)行完畢 count = 0; } } } } 顯示結(jié)果: 例子中我們可以看出當(dāng)個(gè)線(xiàn)程調(diào)用Read方法時(shí),代碼會(huì)先判斷Thread. VolatileRead先讀取計(jì)數(shù)值是否返回正確的計(jì)數(shù)值,,如果正確則顯示 結(jié)果,,不正確的話(huà)繼續(xù)循環(huán)等待,而這個(gè)返回值是通過(guò)其他線(xiàn)程操作Write方法時(shí)最后寫(xiě)入的,,也就是說(shuō)對(duì)于Thread. VolatileWrite 方法的作用便一目了然了,,在實(shí)現(xiàn)Thread. VolatileWrite前寫(xiě)入其他的數(shù)據(jù)或進(jìn)行相應(yīng)的邏輯處理,在我們示例代碼中我們會(huì)先去加運(yùn)算到 10000000時(shí),,通過(guò)thread. VolatileWrite原子性的操作寫(xiě)入計(jì)數(shù)值告訴那個(gè)操作Read方法的線(xiàn)程有一個(gè)計(jì)算任務(wù)已經(jīng)完成,,于是死循環(huán)中 的Thread. VolatileRead方法接受到了信號(hào),你可以顯示計(jì)算結(jié)果了,,于是結(jié)果便會(huì)被顯示,,同時(shí)計(jì)數(shù)值歸零,,這樣便起到了一個(gè)非阻塞功能 的同步效果,,同樣對(duì)于臨界區(qū)(此例中的Write方法體和Read方法體)起到了保護(hù)的作用。當(dāng)然由于使用上述兩個(gè)方法在復(fù)雜的項(xiàng)目中很容易 出錯(cuò),,往往這種錯(cuò)誤是很難被發(fā)現(xiàn),,所以微軟為了讓我們更好使用,便開(kāi)發(fā)出了一個(gè)新的關(guān)鍵字Volatile: Volatile關(guān)鍵字的本質(zhì)含義是告訴編譯器,,聲明為Volatile關(guān)鍵字的變量或字段都是提供給多個(gè)線(xiàn)程使用的,,當(dāng)然不是每個(gè)類(lèi)型都 可以聲明為Volatile類(lèi)型字段,msdn中詳細(xì)說(shuō)明了那些類(lèi)型可以聲明為Volatile 所以不再陳述,但是有一點(diǎn)必須注意,,Volatile 無(wú)法聲明為局部變量,。作為原子性的操作,Volatile關(guān)鍵字具有原子特性,,所以線(xiàn)程間無(wú)法對(duì)其占有,,它的值永遠(yuǎn)是最新的。那我 們就對(duì)上文的那個(gè)例子簡(jiǎn)化如下: View Code
/// <summary> /// 本例利用volatile關(guān)鍵字來(lái)實(shí)現(xiàn)同步,,來(lái)實(shí)現(xiàn)一個(gè)計(jì)算 /// 的例子,,每個(gè)線(xiàn)程負(fù)責(zé)運(yùn)算1000萬(wàn)個(gè)數(shù)據(jù),共開(kāi)啟10個(gè)線(xiàn)程計(jì)算至1億,, /// 而且每個(gè)線(xiàn)程都無(wú)法干擾其他線(xiàn)程工作 /// </summary> class Program { static volatile Int32 count;//計(jì)數(shù)值,,用于線(xiàn)程同步 (注意原子性,所以本例中使用int32) static Int32 value;//實(shí)際運(yùn)算值,,用于顯示計(jì)算結(jié)果 static void Main(string[] args) { //開(kāi)辟一個(gè)線(xiàn)程專(zhuān)門(mén)負(fù)責(zé)讀value的值,,這樣就能看見(jiàn)一個(gè)計(jì)算的過(guò)程 Thread thread2 = new Thread(new ThreadStart(Read)); thread2.Start(); //開(kāi)辟10個(gè)線(xiàn)程來(lái)負(fù)責(zé)計(jì)算,每個(gè)線(xiàn)程負(fù)責(zé)1000萬(wàn)條數(shù)據(jù) for (int i = 0; i < 10; i++) { Thread.Sleep(20); Thread thread = new Thread(new ThreadStart(Write)); thread.Start(); } Console.ReadKey(); } /// <summary> /// 實(shí)際運(yùn)算寫(xiě)操作 /// </summary> private static void Write() { Int32 temp = 0; for (int i = 0; i < 10000000; i++) { temp += 1; } value += temp; //注意VolatileWrite 在每個(gè)線(xiàn)程計(jì)算完畢時(shí)會(huì)寫(xiě)入同步計(jì)數(shù)值為1,,告訴程序該線(xiàn)程已經(jīng)執(zhí)行完畢 //將count值設(shè)置成1,,效果等同于Thread.VolatileWrite count = 1; } /// <summary> /// 顯示計(jì)算后的數(shù)據(jù),使用該方法的線(xiàn)程會(huì)死循環(huán)等待寫(xiě) /// 操作的線(xiàn)程發(fā)出完畢信號(hào)后顯示當(dāng)前計(jì)算結(jié)果 /// </summary> private static void Read() { while (true) { //一旦監(jiān)聽(tīng)到一個(gè)寫(xiě)操作線(xiàn)執(zhí)行完畢后立刻顯示操作結(jié)果,效果等同于Thread.VolatileRead if (count==1) { Console.WriteLine("累計(jì)計(jì)數(shù):{1}", Thread.CurrentThread.ManagedThreadId, value); //將count設(shè)置成0,,等待另一個(gè)線(xiàn)程執(zhí)行完畢 count = 0; } } } } 從例子中大家可以看出Volatile關(guān)鍵字的出現(xiàn)替代了原先VolatileRead 和VolatileWrite方法的繁瑣,,同時(shí)原子性的操作更加直觀(guān)透明
相信大家理解了Volatile后對(duì)于非阻止同步和原子操作有了更深的認(rèn)識(shí),接下來(lái)的Interlocked雖然也屬于非阻止同步但是而后Volatile相比也 有著很大的不同,,interlocked 利用了一個(gè)計(jì)數(shù)值的概念來(lái)實(shí)現(xiàn)同步,,當(dāng)然這個(gè)計(jì)數(shù)值也是屬于原子性的操作,每個(gè)線(xiàn)程都有機(jī)會(huì)通過(guò)Interlocked 去遞增或遞減這個(gè)計(jì)數(shù)值來(lái)達(dá)到同步的效果,同時(shí)Interlocked比Volatile更加適應(yīng)復(fù)雜的邏輯和并發(fā)的情況 首先讓我們了解下Interlocked類(lèi)的一些重要方法
看完了概念性的介紹后,讓我們馬上進(jìn)入很簡(jiǎn)單的一個(gè)示例,,來(lái)深刻理解下Interlocked的使用方法 /// <summary> /// 本示例通過(guò)Interlocked實(shí)現(xiàn)同步示例,,通過(guò)Interlocked.Increment和 /// Interlocked.Decrement來(lái)實(shí)現(xiàn)同步,此例有2個(gè)共享區(qū),,一個(gè)必須滿(mǎn)足計(jì)數(shù)值為0,,另 /// 一個(gè)滿(mǎn)足計(jì)數(shù)值為1時(shí)才能進(jìn)入 /// </summary> class Program { //聲明計(jì)數(shù)變量 //(注意這里用的是long是64位的,所以在32位機(jī)子上一定要通過(guò)Interlocked來(lái)實(shí)現(xiàn)原子操作) static long _count = 0; static void Main(string[] args) { //開(kāi)啟6個(gè)線(xiàn)程,,3個(gè)執(zhí)行Excution1,,三個(gè)執(zhí)行Excution2 for (int i = 0; i < 3; i++) { Thread thread = new Thread(new ThreadStart(Excution1)); Thread thread2 = new Thread(new ThreadStart(Excution2)); thread.Start(); Thread.Sleep(10); thread2.Start(); Thread.Sleep(10); } //這里和同步無(wú)關(guān),只是簡(jiǎn)單的對(duì)Interlocked方法進(jìn)行示例 Interlocked.Add(ref _count, 2); Console.WriteLine("為當(dāng)前計(jì)數(shù)值加上一個(gè)數(shù)量級(jí):{0}后,,當(dāng)前計(jì)數(shù)值為:{1}", 2, _count); Interlocked.Exchange(ref _count, 1); Console.WriteLine("將當(dāng)前計(jì)數(shù)值改變后,,當(dāng)前計(jì)數(shù)值為:{0}", _count); Console.Read(); } static void Excution1() { //進(jìn)入共享區(qū)1的條件 if (Interlocked.Read(ref _count) == 0) { Console.WriteLine("Thread ID:{0} 進(jìn)入了共享區(qū)1", Thread.CurrentThread.ManagedThreadId); //原子性增加計(jì)數(shù)值,讓其他線(xiàn)程進(jìn)入共享區(qū)2 Interlocked.Increment(ref _count); Console.WriteLine("此時(shí)計(jì)數(shù)值Count為:{0}", Interlocked.Read(ref _count)); } } static void Excution2() { //進(jìn)入共享區(qū)2的條件 if (Interlocked.Read(ref _count) == 1) { Console.WriteLine("Thread ID:{0} 進(jìn)入了共享區(qū)2", Thread.CurrentThread.ManagedThreadId); //原子性減少計(jì)數(shù)值,,讓其他線(xiàn)程進(jìn)入共享區(qū)1 Interlocked.Decrement(ref _count); Console.WriteLine("此時(shí)計(jì)數(shù)值Count為:{0}", Interlocked.Read(ref _count)); } } } 在本例中,,我們使用和上文一樣的思路,通過(guò)不同線(xiàn)程來(lái)原子性的操作計(jì)數(shù)值來(lái)達(dá)到同步效果,,大家可以仔細(xì)觀(guān)察到,,通過(guò) Interlocked對(duì)計(jì)數(shù)值進(jìn)行操作就能夠讓我們非常方便的使用非阻止的同步效果了,但是在復(fù)雜的項(xiàng)目或邏輯中,,可能也會(huì)出 錯(cuò)導(dǎo)致活鎖的可能,,大家務(wù)必當(dāng)心
Lock關(guān)鍵字是用來(lái)對(duì)于多線(xiàn)程中的共享區(qū)進(jìn)行阻止同步的一種方案,當(dāng)某一個(gè)線(xiàn)程進(jìn)入臨界區(qū)時(shí),,lock關(guān)鍵字會(huì)鎖住共享區(qū), 同樣可以理解為互斥段,,互斥段在某一時(shí)刻內(nèi)只允許一個(gè)線(xiàn)程進(jìn)入,同時(shí)編譯器會(huì)把這個(gè)關(guān)鍵字編譯成Monitor.Entery和 Monitor.Exit 方法,,關(guān)于Monitor類(lèi)會(huì)在下章詳細(xì)闡述,。既然有Lock關(guān)鍵字,那么它是如何工作的,?到底鎖住了什么,,怎么 高效和正確的使用lock關(guān)鍵字呢? 其實(shí)鎖的概念還是來(lái)自于現(xiàn)實(shí)生活,,共享區(qū)就是多個(gè)人能夠共同擁有房間,,當(dāng)其中一個(gè)人進(jìn)入房間后,他把鎖反鎖,,直到他解鎖 出門(mén)后將鑰匙交給下個(gè)人,,可能房間的門(mén)可能有問(wèn)題,或者進(jìn)入房間的人因?yàn)槟撤N原因出不來(lái)了,,導(dǎo)致全部的人都無(wú)法進(jìn)去,,這些 問(wèn)題也是我們應(yīng)該考慮到的,好,,首先讓我們討論下我們應(yīng)該Lock住什么,,什么材料適合當(dāng)鎖呢? 雖然說(shuō)lock關(guān)鍵字可以鎖住任何object類(lèi)型及其派生類(lèi),,但是盡量不要用public 類(lèi)型的,,因?yàn)閜ublic類(lèi)型難以控制 有可能大伙對(duì)上面的有點(diǎn)疑問(wèn),為什么不能用public類(lèi)型的呢,,為什么會(huì)難以控制呢,? 好,以下3個(gè)例子是比較經(jīng)典的例證
接著這個(gè)例子便是lock(this)的一個(gè)示例,既能讓大伙了解如何使用Lock關(guān)鍵字,,更是讓大伙了解,,lock(this)的危害性 /// <summary> /// 本例展示下如何使用lock關(guān)鍵字和lock(this)時(shí)產(chǎn)生死鎖的情況 /// </summary> class Program { static void Main(string[] args) { //創(chuàng)建b對(duì)象,演示lock B b = new B(); Console.ReadKey(); } } /// <summary> /// A類(lèi)構(gòu)造中初始化一個(gè)線(xiàn)程并且啟動(dòng),, /// 線(xiàn)程調(diào)用的方法內(nèi)放入死循環(huán),,并且在死循環(huán)中放入lock(this), /// </summary> public class A { public A() { Thread th = new Thread(new ThreadStart ( () => { while (true) { lock (this) { Console.WriteLine("進(jìn)入a類(lèi)共享區(qū)"); Thread.Sleep(3000); } } } )); th.Start(); } } /// <summary> /// B類(lèi)在構(gòu)造中創(chuàng)建A的對(duì)象,,并且還是鎖住a對(duì)象,,這樣就創(chuàng)建的死鎖的條件 /// 因?yàn)槌跏蓟疉類(lèi)對(duì)象時(shí),A類(lèi)的構(gòu)造函數(shù)會(huì)鎖住自身對(duì)象,,這樣在A類(lèi)死循環(huán)間隔期,,一旦出了 A類(lèi)中的鎖時(shí) /// 進(jìn)入B的鎖住的區(qū)域內(nèi),,A 對(duì)象永遠(yuǎn)無(wú)法進(jìn)入a類(lèi)共享區(qū),從而產(chǎn)生了死鎖 /// </summary> public class B { public B() { A a = new A(); lock (a) { Console.WriteLine(@"將a類(lèi)對(duì)象鎖住的話(huà),,a類(lèi)中的lock將進(jìn)入死鎖,, 直到3秒后B類(lèi)中的將a類(lèi)對(duì)象釋放鎖,如果我不釋放,那么 a類(lèi)中將永遠(yuǎn)無(wú)法進(jìn)入a類(lèi)共享區(qū)"); //計(jì)時(shí)器 Timer timer = new Timer(new TimerCallback ( (obj) => { Console.WriteLine(DateTime.Now); } ), this, 0, 1000); //如果這里運(yùn)行很長(zhǎng)時(shí)間,, a類(lèi)中將永遠(yuǎn)無(wú)法進(jìn)入a類(lèi)共享區(qū) Thread.Sleep(3000000); } } } 結(jié)果計(jì)時(shí)器會(huì)一直滾動(dòng),,因?yàn)閍對(duì)象被鎖住,除非完成Thread.Sleep(3000000)后才能進(jìn)入到a共享區(qū) 由于以上的問(wèn)題,,微軟還是建議我們使用一個(gè)私有的變量來(lái)鎖定,,由于私有變量外界無(wú)法訪(fǎng)問(wèn),所以鎖住話(huà)死鎖的可能性大大下降了,。 這樣我們就能選擇正確的“門(mén)”來(lái)進(jìn)行鎖住,,但是可能還有一種可能也會(huì)造成死鎖,就是在lock內(nèi)部出現(xiàn)了問(wèn)題,,由于死鎖非常復(fù)雜,,我將在 今后的文章中專(zhuān)門(mén)寫(xiě)一篇關(guān)于死鎖的文章來(lái)深入解釋下死鎖,所以這里就對(duì)死鎖不深究了,,這里大伙了解下lock的使用方法和注意事項(xiàng)就行了,。
由于lock關(guān)鍵字對(duì)臨界區(qū)(共享區(qū))保護(hù)的非常周密,導(dǎo)致了一些功能可能會(huì)無(wú)法實(shí)現(xiàn),,假設(shè)我將某個(gè)查詢(xún)功能放置在臨界區(qū)中時(shí),,可能 當(dāng)別的線(xiàn)程在查詢(xún)臨界區(qū)中的數(shù)據(jù)時(shí),可能我的那個(gè)線(xiàn)程被阻塞了,,所以我們期望鎖能夠達(dá)到以下功能 1 首先鎖能細(xì)分為讀鎖和寫(xiě)鎖 2 能夠保證同時(shí)可以讓多個(gè)線(xiàn)程讀取數(shù)據(jù) 3 能保證同一時(shí)刻只有一個(gè)線(xiàn)程能進(jìn)行寫(xiě)操作,,也就是說(shuō),對(duì)于寫(xiě)操作,,它必須擁有獨(dú)占鎖 4 能保證一個(gè)線(xiàn)程同一時(shí)刻只能擁有寫(xiě)鎖或讀鎖中的一個(gè) 顯然lock關(guān)鍵字無(wú)法滿(mǎn)足我們的需求,,還好微軟想到了這點(diǎn),ReaderWriterLock便隆重登場(chǎng)了ReaderWriterLock能夠達(dá)到的效果是: 1. 同一時(shí)刻,,它允許多個(gè)讀線(xiàn)程同時(shí)訪(fǎng)問(wèn)臨界區(qū),,或者允許單個(gè)線(xiàn)程進(jìn)行寫(xiě)訪(fǎng)問(wèn) 2. 在讀訪(fǎng)問(wèn)率很高,而且寫(xiě)訪(fǎng)問(wèn)率很低的情況下,,效率最高,, 3.它也滿(mǎn)足了同一時(shí)刻只能獲取寫(xiě)鎖或讀鎖的要求。 4. 最為關(guān)鍵的是,,ReaderWriterLock能夠保證讀線(xiàn)程鎖和寫(xiě)線(xiàn)程鎖在各自的讀寫(xiě)隊(duì)列中,,當(dāng)某個(gè)線(xiàn)程釋放了寫(xiě)鎖了,同時(shí)讀線(xiàn)程隊(duì)列中 的所有線(xiàn)程將被授予讀鎖,,同樣,,當(dāng)所有的讀鎖被釋放時(shí),,寫(xiě)線(xiàn)程隊(duì)列中的排隊(duì)的下一個(gè)線(xiàn)程將被授予寫(xiě)鎖,更直觀(guān)的說(shuō),ReaderWriterLock 就是在這幾種狀態(tài)間來(lái)回切換 5 使用時(shí)注意每當(dāng)你使用AcquireXXX方法獲取鎖時(shí),,必須使用ReleaseXXX方法來(lái)釋放鎖 6 ReaderWriterLock 支持遞歸鎖,,關(guān)于遞歸鎖會(huì)在今后的章節(jié)詳細(xì)闡述 7 在性能方面ReaderWriterLock做的不夠理想,,和lock比較差距明顯,,而且該類(lèi)庫(kù)中還隱藏些bug,有于這些原因,微軟又專(zhuān)門(mén)重新寫(xiě)了個(gè)新 類(lèi)ReaderWriterLockSilm來(lái)彌補(bǔ)這些缺陷,。 8 處理死鎖方面ReaderWriterLock為我們提供了超時(shí)的參數(shù)這樣我們便可以有效的防止死鎖 9 對(duì)于一個(gè)個(gè)獲取了讀鎖的線(xiàn)程來(lái)說(shuō),,在寫(xiě)鎖空閑的情況下可以升級(jí)為寫(xiě)鎖
接著讓我們了解下ReaderWriterLock的重要成員 上述4個(gè)方法分別是讓線(xiàn)程獲取寫(xiě)鎖和讀鎖的方法,它利用的計(jì)數(shù)的概念,,當(dāng)一個(gè)線(xiàn)程中調(diào)用此方法后,,該類(lèi)會(huì)給該線(xiàn)程擁有的鎖計(jì)數(shù)加1 (每次加1,但是一個(gè)線(xiàn)程可以擁有多個(gè)讀鎖,,所以計(jì)數(shù)值可能更多,,但是對(duì)于寫(xiě)鎖來(lái)說(shuō)同時(shí)一個(gè)一個(gè)線(xiàn)程可以擁有)。后面的參數(shù)是超時(shí) 時(shí)間,,我們可以自己設(shè)置來(lái)避免死鎖,。同樣調(diào)用上述方法后我們必須使用ReleaseXXX 方法來(lái)讓計(jì)數(shù)值減1,直到該線(xiàn)程擁有鎖的計(jì)數(shù)為0,, 釋放了鎖為止,。 最后我們用一個(gè)簡(jiǎn)單的例子來(lái)溫故下上述的知識(shí)點(diǎn)(請(qǐng)注意看注釋) /// <summary> /// 該示例通過(guò)ReaderWriterLock同步來(lái)實(shí)現(xiàn)Student集合多線(xiàn)程下 /// 的寫(xiě)操作和讀操作 /// </summary> class Program { static ReaderWriterLock _readAndWriteLock = new ReaderWriterLock(); static List<Student> demoList = new List<Student>(); static void Main(string[] args) { InitialStudentList(); Thread thread=null; for (int i = 0; i <5; i++) { //讓第前2個(gè)個(gè)線(xiàn)程試圖掌控寫(xiě)鎖, if (i < 2) { thread = new Thread(new ParameterizedThreadStart(AddStudent)); Console.WriteLine("線(xiàn)程ID:{0}, 嘗試獲取寫(xiě)鎖 ", thread.ManagedThreadId); thread.Start(new Student { Name = "Zhang" + i }); } else { //讓每個(gè)線(xiàn)程都能訪(fǎng)問(wèn)DisplayStudent 方法去獲取讀鎖 thread = new Thread(new ThreadStart(DisplayStudent)); thread.Start(); } Thread.Sleep(20); } Console.ReadKey(); } static void InitialStudentList() { demoList = new List<Student> { new Student{ Name="Sun"}, new Student{Name="Zheng"} }; } /// <summary> /// 當(dāng)多個(gè)線(xiàn)程試圖使用該方法時(shí),只有一個(gè)線(xiàn)程能夠透過(guò)AcquireSWriterLock /// 獲取寫(xiě)鎖,,同時(shí)其他線(xiàn)程進(jìn)入隊(duì)列中等待,,直到該線(xiàn)程使用ReleaseWriterLock后 /// 下個(gè)線(xiàn)程才能進(jìn)入擁有寫(xiě)鎖 /// </summary> /// <param name="student"></param> static void AddStudent(object student) { if (student == null|| !(student is Student)) return; if (demoList.Contains(student)) return; try { //獲取寫(xiě)鎖 _readAndWriteLock.AcquireWriterLock(Timeout.Infinite); demoList.Add(student as Student); Console.WriteLine("當(dāng)前寫(xiě)操作線(xiàn)程為{0}, 寫(xiě)入的學(xué)生是:{1}", Thread.CurrentThread.ManagedThreadId,(student as Student).Name); } catch (Exception) { } finally { _readAndWriteLock.ReleaseWriterLock(); } } /// <summary> /// 對(duì)于讀鎖來(lái)所,允許多個(gè)線(xiàn)程共同擁有,,所以這里同時(shí) /// 可能會(huì)有多個(gè)線(xiàn)程訪(fǎng)問(wèn)Student集合,,使用try catch是為了 /// 一定要讓程序執(zhí)行finally語(yǔ)句塊中的releaseXXX方法,從而保證 /// 能夠釋放鎖 /// </summary> static void DisplayStudent() { try { _readAndWriteLock.AcquireReaderLock(Timeout.Infinite); demoList.ForEach(student => { Console.WriteLine("當(dāng)前集合中學(xué)生為:{0},當(dāng)前讀操作線(xiàn)程為{1}", student.Name, Thread.CurrentThread.ManagedThreadId); }); } catch (Exception) { } finally { _readAndWriteLock.ReleaseReaderLock(); } } } internal class Student { public string Name { get; set; } } 運(yùn)行結(jié)果: 從例子可以看出有2個(gè)線(xiàn)程試圖嘗試爭(zhēng)取寫(xiě)鎖,,但是同時(shí)只有一個(gè)線(xiàn)程可以獲取到寫(xiě)鎖,,同時(shí)對(duì)于讀取集合的線(xiàn)程可以同時(shí)獲取多個(gè)讀鎖
由于本人上個(gè)月工作突然忙了起來(lái),快一個(gè)多月沒(méi)更新博客了,,希望大家可以見(jiàn)諒^^ 本章介紹了線(xiàn)程同步的概念和一些關(guān)于同步非常重要的基本概念,,對(duì)于原子性的操作的認(rèn)識(shí)也格外重要,同時(shí)對(duì)于Volatile,Interlocked,lock,ReaderWriterLock 知識(shí)點(diǎn)做了相關(guān)介紹,, 相信大家對(duì)于線(xiàn)程同步有個(gè)初步的認(rèn)識(shí)和理解,,在寫(xiě)本篇博客時(shí),發(fā)現(xiàn)死鎖也是個(gè)很重要的知識(shí)點(diǎn),,關(guān)于死鎖我會(huì)單獨(dú)寫(xiě)篇文章來(lái)闡述,,謝謝大家的支持,!
CLR via c# msdn |
|
來(lái)自: 賈朋亮博客 > 《線(xiàn)程安全》