C# 讀寫鎖

在多線程編程時(shí)螃成,開發(fā)人員經(jīng)常會(huì)遭遇多個(gè)線程讀寫某個(gè)資源的情況旦签。這就需要進(jìn)行【線程同步】來保證線程安全。一般情況下寸宏,我們的同步措施是使用鎖機(jī)制宁炫。但是,假如線程只對資源進(jìn)行讀取操作氮凝,那么根本不需要使用鎖羔巢;反之,假如線程只對資源進(jìn)行寫入操作罩阵,則應(yīng)當(dāng)使用互斥鎖(比如使用 Monitor 類等)竿秆。還有一種情況,就是存在多個(gè)線程對資源進(jìn)行讀取操作稿壁,同時(shí)每次只有一個(gè)線程對資源進(jìn)行獨(dú)占寫入操作(多用戶讀/單用戶寫) 幽钢。

對一個(gè)對象的讀取次數(shù)遠(yuǎn)遠(yuǎn)大于修改次數(shù),如果只是簡單的用 lock 方式加鎖傅是,則會(huì)影響讀取的效率匪燕。而如果采用讀寫鎖蕾羊,則多個(gè)線程可以同時(shí)讀取該對象,只有等到對象被寫入鎖占用的時(shí)候谎懦,才會(huì)阻塞肚豺。

簡單的說,當(dāng)某個(gè)線程進(jìn)入讀取模式時(shí)界拦,此時(shí)其他線程依然能進(jìn)入讀取模式吸申;假設(shè)此時(shí)一個(gè)線程要進(jìn)入寫入模式,那么他不得不被阻塞享甸,直到讀取模式退出為止截碴。

同樣的,如果某個(gè)線程進(jìn)入了寫入模式蛉威,那么其他線程無論是要寫入還是讀取日丹,都是會(huì)被阻塞的。

ReaderWriterLock 類

.NET Framework BCL 在 1.1 版本時(shí)蚯嫌,給我們提供了一個(gè) ReaderWriterLock 類來面對此種情景哲虾。但是很遺憾,Microsoft 官方不推薦使用該類择示。Jeffrey Richter 也在他的《CLR via C#》一書中對它進(jìn)行了嚴(yán)厲的批判束凑。下面是該類不受歡迎的主要原因:

性能。這個(gè)類實(shí)在是太慢了栅盲。比如它的 AcquireReaderLock 方法比 Monitor 類的 Enter 方法要慢5倍左右汪诉,而等待爭奪寫鎖甚至比Monitor 類慢6倍。

策略谈秫。假如某個(gè)線程完成寫入操作后扒寄,同時(shí)面臨讀線程和寫線程等待處理。ReaderWriterLock 會(huì)優(yōu)先釋放讀線程拟烫,而讓寫線程繼續(xù)等待该编。但我們使用讀寫鎖是因?yàn)榇嬖诖罅康淖x線程和非常少的寫線程,這樣寫線程很可能必須長時(shí)間地等待硕淑,造成寫線程饑餓课竣,不能及時(shí)更新數(shù)據(jù)。更槽糕的情況是喜颁,假如寫線程一直等待稠氮,就會(huì)造成活鎖曹阔。反之半开,我們讓 ReaderWriterLock 采取寫線程優(yōu)先的策略。如果存在多個(gè)寫線程赃份,而讀線程數(shù)量稀少寂拆,也會(huì)造成讀線程饑餓奢米。幸運(yùn)的是,現(xiàn)實(shí)實(shí)踐中纠永,這種情況很少出現(xiàn)鬓长。一旦發(fā)生這種情況,我們可以采取互斥鎖的辦法尝江。

遞歸涉波。ReaderWriterLock 類支持鎖遞歸。這就意味著該鎖清楚的知道目前哪個(gè)線程擁有它炭序。假如擁有該鎖的線程遞歸嘗試獲得該讀寫鎖啤覆,遞歸算法允許該線程獲得該讀寫鎖,并且增加獲得該鎖的計(jì)數(shù)惭聂。然而該線程必須釋放該鎖相同的次數(shù)以便線程不再擁有該鎖窗声。盡管這看起來是個(gè)很好的特性,但是實(shí)現(xiàn)這個(gè)“特性”代價(jià)太高辜纲。首先笨觅,因?yàn)槎鄠€(gè)讀線程可以同時(shí)擁有該讀寫鎖,這必須讓該鎖為每個(gè)線程保持計(jì)數(shù)耕腾。此外见剩,還需要額外的內(nèi)存空間和時(shí)間來更新計(jì)數(shù)。這個(gè)特性對 ReaderWriterLock 類可憐的性能貢獻(xiàn)極大幽邓。其次炮温,有些良好的設(shè)計(jì)需要一個(gè)線程在此處獲得該鎖,然后在別處釋放該鎖(比如 .NET 的異步編程架構(gòu))牵舵。因?yàn)檫@個(gè)遞歸特性柒啤,ReaderWriterLock 不支持這種編程架構(gòu)。

資源泄漏畸颅。在 .NET 2.0 之前的版本中担巩, ReaderWriterLock 類會(huì)造成內(nèi)核對象泄露。這些對象只有在進(jìn)程終止后才能再次回收没炒。幸運(yùn)的是涛癌,.NET 2.0 修正了這個(gè) Bug 。

此外送火,ReaderWriterLock 還有個(gè)令人擔(dān)心的危險(xiǎn):非原子性操作拳话。它就是 UpgradeToWriteLock 方法。這個(gè)方法實(shí)際上在更新到寫鎖前先釋放了讀鎖种吸。這就讓其他線程有機(jī)會(huì)在此期間乘虛而入弃衍,從而獲得讀寫鎖且改變狀態(tài)。如果先更新到寫鎖坚俗,然后釋放讀鎖镜盯,假如兩個(gè)線程同時(shí)更新將會(huì)導(dǎo)致另外一個(gè)線程死鎖岸裙。

所以 Microsoft 決定構(gòu)建一個(gè)新類來一次性解決上述所有問題,這就是 ReaderWriterLockSlim 類速缆。本來可以在原有的 ReaderWriterLock 類上修正錯(cuò)誤降允,但是考慮到兼容性和已存在的 API ,Microsoft 放棄了這種做法艺糜。當(dāng)然也可以標(biāo)記 ReaderWriterLock 類為 Obsolete剧董,但是由于某些原因,這個(gè)類還有存在的必要破停。

ReaderWriterLockSlim 類

表示用于管理資源訪問的鎖定狀態(tài)送滞,可實(shí)現(xiàn)多線程讀取或進(jìn)行獨(dú)占式寫入訪問。

使用?ReaderWriterLockSlim?來保護(hù)由多個(gè)線程讀取但每次只采用一個(gè)線程寫入的資源辱挥。?ReaderWriterLockSlim?允許多個(gè)線程均處于讀取模式犁嗅,允許一個(gè)線程處于寫入模式并獨(dú)占鎖定狀態(tài),同時(shí)還允許一個(gè)具有讀取權(quán)限的線程處于可升級(jí)的讀取模式晤碘,在此模式下線程無需放棄對資源的讀取權(quán)限即可升級(jí)為寫入模式褂微。

這個(gè)新的讀寫鎖類性能跟 Monitor 類大致相當(dāng),大概在 Monitor 類的 2 倍之內(nèi)园爷。而且新鎖優(yōu)先讓寫線程獲得鎖宠蚂,因?yàn)閷懖僮鞯念l率遠(yuǎn)小于讀操作。通常這會(huì)導(dǎo)致更好的可伸縮性童社。起初求厕,ReaderWriterLockSlim 類在設(shè)計(jì)時(shí)考慮到相當(dāng)多的情況。比如在早期 CTP 的代碼還提供了PrefersReaders扰楼, PrefersWritersAndUpgrades 和 Fifo 等競爭策略呀癣。但是這些策略雖然添加起來非常簡單,但是會(huì)導(dǎo)致情況非常的復(fù)雜弦赖。所以 Microsoft 最后決定提供一個(gè)能夠在大多數(shù)情況下良好工作的簡單模型项栏。

注意?ReaderWriterLockSlim 類似于 ReaderWriterLock,只是簡化了遞歸蹬竖、升級(jí)和降級(jí)鎖定狀態(tài)的規(guī)則沼沈。 ReaderWriterLockSlim 可避免多種潛在的死鎖情況。 此外币厕,ReaderWriterLockSlim 的性能明顯優(yōu)于 ReaderWriterLock列另。 建議在所有新的開發(fā)工作中使用 ReaderWriterLockSlim。

默認(rèn)情況下 ReaderWriterLockSlim 的新實(shí)例使用 LockRecursionPolicy.NoRecursion 標(biāo)志創(chuàng)建旦装,并不允許遞歸页衙。 對于所有新開發(fā),建議使用此默認(rèn)策略同辣,因?yàn)檫f歸帶來不必要的復(fù)雜情況拷姿,從而使您的代碼更容易出現(xiàn)死鎖。 若要簡化從現(xiàn)有的項(xiàng)目使用 Monitor 或 ReaderWriterLock旱函,您可以使用 LockRecursionPolicy.SupportsRecursion 標(biāo)志來創(chuàng)建 ReaderWriterLockSlim 的實(shí)例 响巢,允許使用遞歸。

一個(gè)線程可以進(jìn)入鎖定狀態(tài)的三種模式︰ 讀取模式棒妨、 寫入模式和可升級(jí)模式(可升級(jí)的讀取模式 )踪古。

ReaderWriterLockSlim 類提供了可升級(jí)模式,這種模式通常適用于在其中一個(gè)線程讀取受保護(hù)資源的情況下券腔,如果滿足某個(gè)條件伏穆,可能需要對其進(jìn)行寫入。 這種方式和讀取模式的區(qū)別在于它可以通過調(diào)用 EnterWriteLock 或 TryEnterWriteLock 方法升級(jí)為寫入模式纷纫。 因?yàn)槊看沃荒苡幸粋€(gè)線程處于可升級(jí)模式枕扫。進(jìn)入可升級(jí)模式的線程,不會(huì)影響讀取模式的線程辱魁,即當(dāng)一個(gè)線程進(jìn)入了可升級(jí)模式烟瞧,任意數(shù)量的線程可以同時(shí)進(jìn)入讀取模式,不會(huì)阻塞染簇。如果有多個(gè)線程已經(jīng)在等待獲取寫入鎖参滴,那么運(yùn)行 EnterUpgradeableReadLock 將會(huì)阻塞,直到那些線程超時(shí)或者退出寫入鎖锻弓。

ReaderWriterLockSlim 具有托管線程關(guān)聯(lián)砾赔;也就是說,每個(gè) Thread 對象必須使用自己的方法調(diào)用進(jìn)入和退出鎖模式青灼。 任何線程都不可以更改另一個(gè)線程的模式暴心。

ReaderWriterLockSlim 的更新鎖

現(xiàn)在讓我們更加深入的討論一下更新模型。UpgradeableRead 鎖定模式允許安全的降級(jí)到 Read 模式或升級(jí)到 Write 模式杂拨。還記得先前 ReaderWriterLock 的更新是非原子性酷勺,危險(xiǎn)的操作嗎(尤其是大多數(shù)人根本沒有意識(shí)到這點(diǎn))?現(xiàn)在提供的新讀寫鎖既不會(huì)破壞原子性扳躬,也不會(huì)導(dǎo)致死鎖脆诉。新鎖一次只允許一個(gè)線程處在 UpgradeableRead 模式下。

一旦該讀寫鎖處在 UpgradeableRead 模式下贷币,線程就能讀取某些狀態(tài)值來決定是否降級(jí)到 Read 模式或升級(jí)到 Write 模式击胜。遺憾的是,CLR 團(tuán)隊(duì)移除了 DowngradeToRead 和 UpgradeToWrite 兩個(gè)方法役纹。如果要降級(jí)到讀鎖偶摔,只要簡單調(diào)用 EnterReadLock 方法,然后再調(diào)用 ExitUpgradeableReadLock 方法即可促脉。如果要升級(jí)到寫鎖辰斋,只要簡單調(diào)用 EnterWriteLock 方法即可:這可能要等待策州,直到不再有任何線程在 Read 模式下持有鎖。

ReaderWriterLockSlim 的遞歸策略

新的讀寫鎖還有一個(gè)有意思的特性就是它的遞歸策略宫仗。默認(rèn)情況下够挂,除已提及的降級(jí)到讀鎖和升級(jí)到寫鎖之外,所有的遞歸請求都不允許藕夫。這意味著你不能連續(xù)兩次調(diào)用 EnterReadLock孽糖,其他模式下也類似。如果你這么做毅贮,CLR 將會(huì)拋出 LockRecursionException 異常办悟。當(dāng)然,你可以使用 LockRecursionPolicy.SupportsRecursion 的構(gòu)造函數(shù)參數(shù)讓該讀寫鎖支持遞歸鎖定滩褥。但不建議對新的開發(fā)使用遞歸病蛉,因?yàn)檫f歸會(huì)帶來不必要的復(fù)雜情況,從而使你的代碼更容易出現(xiàn)死鎖現(xiàn)象瑰煎。

有一種特殊的情況永遠(yuǎn)也不被允許铡恕,無論你采取什么樣的遞歸策略。這就是當(dāng)線程持有讀鎖時(shí)請求寫鎖丢间。Microsoft 曾經(jīng)考慮提供這樣的支持探熔,但是這種情況太容易導(dǎo)致死鎖。所以 Microsoft 最終放棄了這個(gè)方案烘挫。

此外诀艰,這個(gè)新的讀寫鎖還提供了很多對應(yīng)的屬性來確定線程是否在指定模型下持有該鎖。比如 IsReadLockHeld饮六, IsWriteLockHeld 和 IsUpgradeableReadLockHeld 其垄。你也可以通過 WaitingReadCount,WaitingWriteCount 和 WaitingUpgradeCount 等屬性來查看有多少線程正在等待持有特定模式下的鎖卤橄。CurrentReadCount 屬性則告知目前有多少并發(fā)讀線程绿满。RecursiveReadCount, RecursiveWriteCount 和 RecursiveUpgradeCount 則告知目前線程進(jìn)入特定模式鎖定狀態(tài)下的次數(shù)窟扑。

小結(jié)

這篇文章分析了 .NET 中提供的兩個(gè)讀寫鎖類喇颁。然而 .NET 3.5 提供的新讀寫鎖 ReaderWriterLockSlim 類消除了 ReaderWriterLock 類存在的主要問題。與 ReaderWriterLock 相比嚎货,性能有了極大提高橘霎。更新具有原子性臣镣,也可以極大避免死鎖谬返。更有清晰的遞歸策略。在任何情況下延柠,我們都應(yīng)該使用 ReaderWriterLockSlim 類來代替 ReaderWriterLock 類。

如果應(yīng)用場景要求性能十分苛刻外潜,可以考慮采用 lock-free 方案原环。但是 lock-free 有著固有缺陷:極難編碼,極難證明其正確性处窥。讀寫鎖方案的應(yīng)用范圍更加廣泛一些嘱吗。

讀寫鎖有個(gè)很常用的場景就是在緩存設(shè)計(jì)中。因?yàn)榫彺嬷薪?jīng)常有些很穩(wěn)定碧库,不太長更新的內(nèi)容。MSDN 的代碼示例就很經(jīng)典巧勤,我原版拷貝一下嵌灰,呵呵。代碼示例如下:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颅悉,一起剝皮案震驚了整個(gè)濱河市沽瞭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌剩瓶,老刑警劉巖驹溃,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異延曙,居然都是意外死亡豌鹤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門枝缔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來布疙,“玉大人,你說我怎么就攤上這事愿卸×榱伲” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵趴荸,是天一觀的道長儒溉。 經(jīng)常有香客問我,道長发钝,這世上最難降的妖魔是什么顿涣? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮酝豪,結(jié)果婚禮上园骆,老公的妹妹穿的比我還像新娘。我一直安慰自己寓调,他們只是感情好锌唾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般晌涕。 火紅的嫁衣襯著肌膚如雪滋捶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天余黎,我揣著相機(jī)與錄音重窟,去河邊找鬼。 笑死惧财,一個(gè)胖子當(dāng)著我的面吹牛巡扇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播垮衷,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼厅翔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了搀突?” 一聲冷哼從身側(cè)響起刀闷,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仰迁,沒想到半個(gè)月后甸昏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡徐许,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年施蜜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雌隅。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡花墩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出澄步,到底是詐尸還是另有隱情冰蘑,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布村缸,位于F島的核電站祠肥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏梯皿。R本人自食惡果不足惜仇箱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望东羹。 院中可真熱鬧剂桥,春花似錦、人聲如沸属提。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至斟薇,卻和暖如春师坎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背堪滨。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工胯陋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人袱箱。 一個(gè)月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓遏乔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親发笔。 傳聞我的和親對象是個(gè)殘疾皇子盟萨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容