C# 讀寫鎖

在多線程編程時(shí),開發(fā)人員經(jīng)常會遭遇多個(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 方式加鎖,則會影響讀取的效率。而如果采用讀寫鎖,則多個(gè)線程可以同時(shí)讀取該對象,只有等到對象被寫入鎖占用的時(shí)候,才會阻塞。

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

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

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 會優(yōu)先釋放讀線程,而讓寫線程繼續(xù)等待。但我們使用讀寫鎖是因?yàn)榇嬖诖罅康淖x線程和非常少的寫線程,這樣寫線程很可能必須長時(shí)間地等待,造成寫線程饑餓,不能及時(shí)更新數(shù)據(jù)。更槽糕的情況是,假如寫線程一直等待,就會造成活鎖。反之,我們讓 ReaderWriterLock 采取寫線程優(yōu)先的策略。如果存在多個(gè)寫線程,而讀線程數(shù)量稀少,也會造成讀線程饑餓。幸運(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 類會造成內(nèi)核對象泄露。這些對象只有在進(jìn)程終止后才能再次回收。幸運(yùn)的是,.NET 2.0 修正了這個(gè) Bug 。

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

所以 Microsoft 決定構(gòu)建一個(gè)新類來一次性解決上述所有問題,這就是 ReaderWriterLockSlim 類。本來可以在原有的 ReaderWriterLock 類上修正錯誤,但是考慮到兼容性和已存在的 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)限的線程處于可升級的讀取模式,在此模式下線程無需放棄對資源的讀取權(quán)限即可升級為寫入模式。

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

注意?ReaderWriterLockSlim 類似于 ReaderWriterLock,只是簡化了遞歸、升級和降級鎖定狀態(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)的三種模式︰ 讀取模式、 寫入模式和可升級模式(可升級的讀取模式 )。

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

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

ReaderWriterLockSlim 的更新鎖

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

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

ReaderWriterLockSlim 的遞歸策略

新的讀寫鎖還有一個(gè)有意思的特性就是它的遞歸策略。默認(rèn)情況下,除已提及的降級到讀鎖和升級到寫鎖之外,所有的遞歸請求都不允許。這意味著你不能連續(xù)兩次調(diào)用 EnterReadLock,其他模式下也類似。如果你這么做,CLR 將會拋出 LockRecursionException 異常。當(dāng)然,你可以使用 LockRecursionPolicy.SupportsRecursion 的構(gòu)造函數(shù)參數(shù)讓該讀寫鎖支持遞歸鎖定。但不建議對新的開發(fā)使用遞歸,因?yàn)檫f歸會帶來不必要的復(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容