本文提供此 API 參考文件的補充備註。
使用 ReaderWriterLockSlim 來保護由多個線程讀取但一次僅由一個線程寫入的資源。 ReaderWriterLockSlim 允許多個線程處於讀取模式,允許一個線程處於具有鎖定獨佔擁有權的寫入模式,並允許具有可升級讀取存取權的線程進入可升級的讀取模式,線程可以升級至寫入模式,而不需要放棄對資源的讀取存取權。
備註
- ReaderWriterLockSlim 類似於 ReaderWriterLock,但它已簡化遞歸規則,以及升級和降級鎖定狀態的規則。 ReaderWriterLockSlim 可避免許多潛在的死結案例。 此外,效能 ReaderWriterLockSlim 明顯優於 ReaderWriterLock。 ReaderWriterLockSlim 被建議用於所有新的開發。
- ReaderWriterLockSlim 不具備線程中止安全性。 您不應該在可中止存取線程的環境中使用它,例如 .NET Framework。 如果您使用 .NET Core 或 .NET 5+,則應該沒問題。 Abort .NET Core 不支援 ,而且在 .NET 5 和更新版本中 已過時 。
根據預設,新的 ReaderWriterLockSlim 實例會使用 LockRecursionPolicy.NoRecursion 旗標建立,並且不允許遞歸。 建議針對所有新的開發使用此預設原則,因為遞歸會造成不必要的複雜問題,並讓您的程式代碼更容易發生死結。 若要簡化從使用 Monitor 或 ReaderWriterLock的現有項目移轉,您可以使用 LockRecursionPolicy.SupportsRecursion 旗標建立允許遞歸的 ReaderWriterLockSlim 實例。
線程可以進入鎖的三種模式:讀取模式、寫入模式和升級型讀取模式。 (在本主題的其餘部分,「可升級的讀取模式」稱為「可升級模式」,而「進入 x 模式」片語則優先於較長片語「以 x 模式進入鎖定」。)
不論遞歸原則為何,隨時只能有一個線程處於寫入模式。 當線程處於寫入模式時,沒有任何其他線程可以在任何模式中進入鎖定。 隨時只能有一個線程處於可升級模式。 任意數目的線程都可以處於讀取模式,而且在可升級模式中可以有一個線程,而其他線程則處於讀取模式。
這很重要
此類型會實作 IDisposable 介面。 當您完成使用這個物品後,應直接或間接地處理它。 若要直接處置類型,請在 Disposetry/ 區塊中呼叫其 catch 方法。 若要間接處置它,請使用語言建構,例如 using (C#) 或 Using (在 Visual Basic 中)。 如需詳細資訊,請參閱介面主題中的
ReaderWriterLockSlim 具有受控線程關聯性;也就是說,每個 Thread 對象必須自行調用方法,才能進入和結束鎖定模式。 沒有線程可以變更另一個線程的模式。
如果 ReaderWriterLockSlim 不允許遞歸,嘗試進入鎖的線程可能會因數個原因而被阻塞:
如果有線程正在等候進入寫模式,或單個線程處於寫模式中,嘗試進入讀取模式的線程會被阻塞。
備註
當寫入器排入佇列時,封鎖新的讀取器是一項有利於寫入器的鎖定公平策略。 目前的公平性政策在讀者和作者之間取得公平平衡,以促進在最常見場景中的流量。 未來的 .NET 版本可能會引入新的公平性原則。
如果有線程正在可升級模式中、有正在等待進入寫入模式的線程,或寫入模式中有單一線程,則嘗試進入可升級模式的線程會被封鎖。
當試圖進入寫入模式的線程發現任何一種模式中已有線程存在時,將會被阻塞。
升級和降級鎖
可升級的模式適用於當線程通常從受保護的資源進行讀取時,但在某些條件滿足時,可能需要對該資源進行寫入。 在可升級模式中進入 ReaderWriterLockSlim 的線程具有受保護資源的讀取許可權,而且可以藉由呼叫 EnterWriteLock 或 TryEnterWriteLock 方法升級至寫入模式。 因為一次只能有一個線程處於可升級模式,所以在默認不允許遞歸的情況下,升級到寫入模式時無法發生死結。
這很重要
不論遞歸原則為何,一開始進入讀取模式的線程都不允許升級為可升級模式或寫入模式,因為該模式會產生強機率的死結。 例如,如果讀取模式中的兩個線程都嘗試進入寫入模式,它們就會死結。 可升級模式的設計是為了避免這類死結。
如果讀取模式中有其他線程,正在升級的線程會被封鎖。 當線程遭到封鎖時,會封鎖嘗試進入讀取模式的其他線程。 當所有線程都結束讀取模式時,封鎖的可升級線程會進入寫入模式。 如果有其他線程等待進入寫入模式,它們仍會遭到封鎖,因為處於可升級模式的單一線程會防止它們取得資源的獨佔存取權。
當可升級模式中的線程結束寫入模式時,除非有線程等候進入寫入模式,否則等候進入讀取模式的其他線程可以執行此動作。 可升級模式中的線程可以無限期升級和降級,只要它是寫入受保護資源的唯一線程。
這很重要
如果您允許多個線程進入寫入模式或可升級模式,則不得允許一個線程壟斷可升級模式。 否則,嘗試直接進入寫入模式的線程將會無限期封鎖,而當線程遭到封鎖時,其他線程將無法進入讀取模式。
可升級模式中的線程可以先呼叫 EnterReadLock 方法,然後呼叫 ExitUpgradeableReadLock 方法,降級為讀取模式。 所有鎖定遞歸原則都允許此降級模式,即使是 NoRecursion。
降級至讀取模式之後,線程在結束讀取模式之前,無法重新進入可升級模式。
以遞歸方式輸入鎖定
您可以透過使用指定鎖定原則的建構函數來建立支援遞歸鎖定的ReaderWriterLockSlim,並指定ReaderWriterLockSlim(LockRecursionPolicy)。
備註
不建議針對新的開發使用遞歸,因為它引進不必要的複雜問題,並讓您的程式代碼更容易發生死結。
允許遞迴的ReaderWriterLockSlim,線程可以進入的模式如下所述:
讀取模式中的線程可以遞歸進入讀取模式,但無法進入寫入模式或可升級模式。 如果嘗試這樣做,則會拋出 LockRecursionException。 進入讀取模式,然後進入寫入模式或可升級模式是具有強機率死結的模式,因此不允許。 如先前所述,針對需要升級鎖定的情況,會提供可升級模式。
可升級模式的線程可以進入寫入模式和/或讀取模式,而且可以遞歸地輸入這三種模式中的任何一種。 不過,如果有其他線程處於讀模式中,嘗試進入寫模式時會被阻塞。
寫入模式中的線程可以進入讀取模式和/或可升級模式,而且可以遞歸地輸入這三種模式中的任何一種。
未進入鎖定的線程可以進入任何模式。 此嘗試可能會出於與嘗試進入非遞歸鎖相同的原因而被阻止。
線程可以以任何順序退出其進入的模式,只要它退出每個模式的次數與進入該模式的次數完全相同。 如果線程嘗試結束模式太多次,或結束尚未進入的模式, SynchronizationLockException 則會擲回 。
鎖定狀態
您可以從鎖的狀態來考慮,這可能會很有用。 ReaderWriterLockSlim可以是四種狀態之一:未輸入、讀取、升級和寫入。
未輸入:在此狀態下,沒有線程進入鎖定(或所有線程都已結束鎖定)。
讀取:在此狀態下,一或多個線程已進入鎖定以讀取受保護資源。
備註
線程可以使用 EnterReadLock 或 TryEnterReadLock 方法,或從可升級模式降級,進入讀取模式下的鎖。
升級:在此狀態下,一個執行緒已進入鎖定以取得讀取存取權,並可選擇升級為寫入存取權(也就是處於可升級模式),而零或多個執行緒已進入鎖定以進行讀取存取。 一次只能有一個線程進入鎖定,且具有升級選項;嘗試進入可升級模式的額外線程會被阻擋。
寫入:在此狀態下,一個線程已進入鎖定以寫入受保護資源。 該線程具有鎖定的獨佔擁有權。 因為任何原因而嘗試進入鎖定的任何其他線程會遭到封鎖。
下表描述了不允許遞歸的鎖在鎖定狀態之間的轉換,當線程 t 採取最左欄中所述的動作時。 在採取動作時, t 沒有模式。 在表格註腳中描述了特殊案例中 t 處於可升級模式的情況。頂端行描述了鎖定的初始狀態。 單元格會描述線程會發生什麼事,並顯示括弧中鎖定狀態的變更。
| 過渡 | 未輸入 (N) | 讀取 (R) | 升級 (U) | 寫入 (W) |
|---|---|---|---|---|
t 進入讀取模式 |
t enters (R)。 |
t 如果線程正在等候寫入模式,則會阻塞;否則,t 進入。 |
t如果執行緒正在等待寫模式,則會阻塞;否則,t進入1。 |
t 區塊。 |
t 進入可升級模式 |
t 進入(U)。 |
t 如果有線程正在等候寫入模式或升級模式,則會阻塞;否則,t 進入(U)。 |
t 區塊。 |
t 區塊。 |
t 進入寫入模式 |
t 輸入 (W)。 |
t 區塊。 |
t 塊。2 |
t 區塊。 |
1 如果 t 以可升級模式啟動,則會進入讀取模式。 此動作永遠不會封鎖。 鎖定狀態不會變更。 (線程接著可以透過退出可升級模式,完成降級至讀取模式。)
2 如果 t 以可升級模式啟動,它會封鎖讀取模式中的線程。 否則它會升級為寫入模式。 鎖定狀態會變更為 Write (W)。 如果 t 阻塞是因為有線程處於讀取模式,則當最後一個線程結束讀取模式時,它就會進入寫入模式,即使有線程等候進入寫入模式也一樣。
當狀態變更因為線程結束鎖定而發生時,會選取要喚醒的下一個線程,如下所示:
- 首先,正在等候寫入模式且已處於可升級模式的線程(最多可以有一個這類線程)。
- 若不成功,正在等候進入寫入模式的執行緒。
- 如果不成功,正在等候可升級模式的線程。
- 如果未達成條件,所有正在等待讀取模式的線程將如何處理。
在前兩個案例中,鎖定的後續狀態一律為 Write (W),而第三個案例中的 Upgrade (U)則不論結束線程觸發狀態變更時鎖定的狀態為何。 在最後一個案例中,如果狀態變更之後有線程處於可升級模式,則鎖定的狀態為 Upgrade (U),否則為 Read (R),而不論先前的狀態為何。
範例
下列範例顯示一個簡單的同步的快取,用於儲存具有整數鍵的字串。 ReaderWriterLockSlim 的實體用於同步對作為內部快取的 Dictionary<TKey,TValue> 的存取。
此範例包含新增至快取、從快取刪除,以及從快取讀取的簡單方法。 為了展示逾時,此範例包含一個方法,只有在能夠在指定的逾時範圍內成功執行時,才會將資料新增至快取。
為了示範可升級模式,此範例包含方法,可擷取與索引鍵相關聯的值,並將其與新的值進行比較。 如果值未變更,方法會傳回指出沒有變更的狀態。 如果找不到索引鍵的值,則會插入索引鍵/值組。 如果值已變更,則會更新此值。 可升級模式可讓線程視需要從讀取許可權升級為寫入存取權,而不會有死結的風險。
此範例包含巢狀列舉,指定示範可升級模式之方法的傳回值。
此範例會使用無參數建構函式來建立鎖定,因此不允許遞歸。 當鎖定不允許遞歸時,程式設計 ReaderWriterLockSlim 會比較簡單且較不容易發生錯誤。
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks
public class SynchronizedCache
{
private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary<int, string> innerCache = new Dictionary<int, string>();
public int Count
{ get { return innerCache.Count; } }
public string Read(int key)
{
cacheLock.EnterReadLock();
try
{
return innerCache[key];
}
finally
{
cacheLock.ExitReadLock();
}
}
public void Add(int key, string value)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public bool AddWithTimeout(int key, string value, int timeout)
{
if (cacheLock.TryEnterWriteLock(timeout))
{
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return true;
}
else
{
return false;
}
}
public AddOrUpdateStatus AddOrUpdate(int key, string value)
{
cacheLock.EnterUpgradeableReadLock();
try
{
string result = null;
if (innerCache.TryGetValue(key, out result))
{
if (result == value)
{
return AddOrUpdateStatus.Unchanged;
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache[key] = value;
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Updated;
}
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Added;
}
}
finally
{
cacheLock.ExitUpgradeableReadLock();
}
}
public void Delete(int key)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Remove(key);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public enum AddOrUpdateStatus
{
Added,
Updated,
Unchanged
};
~SynchronizedCache()
{
if (cacheLock != null) cacheLock.Dispose();
}
}
Public Class SynchronizedCache
Private cacheLock As New ReaderWriterLockSlim()
Private innerCache As New Dictionary(Of Integer, String)
Public ReadOnly Property Count As Integer
Get
Return innerCache.Count
End Get
End Property
Public Function Read(ByVal key As Integer) As String
cacheLock.EnterReadLock()
Try
Return innerCache(key)
Finally
cacheLock.ExitReadLock()
End Try
End Function
Public Sub Add(ByVal key As Integer, ByVal value As String)
cacheLock.EnterWriteLock()
Try
innerCache.Add(key, value)
Finally
cacheLock.ExitWriteLock()
End Try
End Sub
Public Function AddWithTimeout(ByVal key As Integer, ByVal value As String, _
ByVal timeout As Integer) As Boolean
If cacheLock.TryEnterWriteLock(timeout) Then
Try
innerCache.Add(key, value)
Finally
cacheLock.ExitWriteLock()
End Try
Return True
Else
Return False
End If
End Function
Public Function AddOrUpdate(ByVal key As Integer, _
ByVal value As String) As AddOrUpdateStatus
cacheLock.EnterUpgradeableReadLock()
Try
Dim result As String = Nothing
If innerCache.TryGetValue(key, result) Then
If result = value Then
Return AddOrUpdateStatus.Unchanged
Else
cacheLock.EnterWriteLock()
Try
innerCache.Item(key) = value
Finally
cacheLock.ExitWriteLock()
End Try
Return AddOrUpdateStatus.Updated
End If
Else
cacheLock.EnterWriteLock()
Try
innerCache.Add(key, value)
Finally
cacheLock.ExitWriteLock()
End Try
Return AddOrUpdateStatus.Added
End If
Finally
cacheLock.ExitUpgradeableReadLock()
End Try
End Function
Public Sub Delete(ByVal key As Integer)
cacheLock.EnterWriteLock()
Try
innerCache.Remove(key)
Finally
cacheLock.ExitWriteLock()
End Try
End Sub
Public Enum AddOrUpdateStatus
Added
Updated
Unchanged
End Enum
Protected Overrides Sub Finalize()
If cacheLock IsNot Nothing Then cacheLock.Dispose()
End Sub
End Class
下列程式代碼接著會使用 SynchronizedCache 對象來儲存蔬菜名稱的字典。 它會建立三個任務。 第一個會將儲存在陣列 SynchronizedCache 中的蔬菜名稱寫入實例。 第二個和第三個任務顯示蔬菜的名稱,第一個以遞增順序(從低索引到高索引),第二個以遞減順序排列。 最後一項工作會搜尋字串 「cucumber」 ,並在找到字串時呼叫 EnterUpgradeableReadLock 方法來取代字串 「green bean」。
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks
public class Example
{
public static void Main()
{
var sc = new SynchronizedCache();
var tasks = new List<Task>();
int itemsWritten = 0;
// Execute a writer.
tasks.Add(Task.Run( () => { String[] vegetables = { "broccoli", "cauliflower",
"carrot", "sorrel", "baby turnip",
"beet", "brussel sprout",
"cabbage", "plantain",
"spinach", "grape leaves",
"lime leaves", "corn",
"radish", "cucumber",
"raddichio", "lima beans" };
for (int ctr = 1; ctr <= vegetables.Length; ctr++)
sc.Add(ctr, vegetables[ctr - 1]);
itemsWritten = vegetables.Length;
Console.WriteLine($"Task {Task.CurrentId} wrote {itemsWritten} items\n");
} ));
// Execute two readers, one to read from first to last and the second from last to first.
for (int ctr = 0; ctr <= 1; ctr++) {
bool desc = ctr == 1;
tasks.Add(Task.Run( () => { int start, last, step;
int items;
do {
String output = String.Empty;
items = sc.Count;
if (! desc) {
start = 1;
step = 1;
last = items;
}
else {
start = items;
step = -1;
last = 1;
}
for (int index = start; desc ? index >= last : index <= last; index += step)
output += String.Format("[{0}] ", sc.Read(index));
Console.WriteLine($"Task {Task.CurrentId} read {items} items: {output}\n");
} while (items < itemsWritten | itemsWritten == 0);
} ));
}
// Execute a red/update task.
tasks.Add(Task.Run( () => { Thread.Sleep(100);
for (int ctr = 1; ctr <= sc.Count; ctr++) {
String value = sc.Read(ctr);
if (value == "cucumber")
if (sc.AddOrUpdate(ctr, "green bean") != SynchronizedCache.AddOrUpdateStatus.Unchanged)
Console.WriteLine("Changed 'cucumber' to 'green bean'");
}
} ));
// Wait for all three tasks to complete.
Task.WaitAll(tasks.ToArray());
// Display the final contents of the cache.
Console.WriteLine();
Console.WriteLine("Values in synchronized cache: ");
for (int ctr = 1; ctr <= sc.Count; ctr++)
Console.WriteLine($" {ctr}: {sc.Read(ctr)}");
}
}
// The example displays the following output:
// Task 1 read 0 items:
//
// Task 3 wrote 17 items
//
//
// Task 1 read 17 items: [broccoli] [cauliflower] [carrot] [sorrel] [baby turnip] [
// beet] [brussel sprout] [cabbage] [plantain] [spinach] [grape leaves] [lime leave
// s] [corn] [radish] [cucumber] [raddichio] [lima beans]
//
// Task 2 read 0 items:
//
// Task 2 read 17 items: [lima beans] [raddichio] [cucumber] [radish] [corn] [lime
// leaves] [grape leaves] [spinach] [plantain] [cabbage] [brussel sprout] [beet] [b
// aby turnip] [sorrel] [carrot] [cauliflower] [broccoli]
//
// Changed 'cucumber' to 'green bean'
//
// Values in synchronized cache:
// 1: broccoli
// 2: cauliflower
// 3: carrot
// 4: sorrel
// 5: baby turnip
// 6: beet
// 7: brussel sprout
// 8: cabbage
// 9: plantain
// 10: spinach
// 11: grape leaves
// 12: lime leaves
// 13: corn
// 14: radish
// 15: green bean
// 16: raddichio
// 17: lima beans
Public Module Example
Public Sub Main()
Dim sc As New SynchronizedCache()
Dim tasks As New List(Of Task)
Dim itemsWritten As Integer
' Execute a writer.
tasks.Add(Task.Run( Sub()
Dim vegetables() As String = { "broccoli", "cauliflower",
"carrot", "sorrel", "baby turnip",
"beet", "brussel sprout",
"cabbage", "plantain",
"spinach", "grape leaves",
"lime leaves", "corn",
"radish", "cucumber",
"raddichio", "lima beans" }
For ctr As Integer = 1 to vegetables.Length
sc.Add(ctr, vegetables(ctr - 1))
Next
itemsWritten = vegetables.Length
Console.WriteLine("Task {0} wrote {1} items{2}",
Task.CurrentId, itemsWritten, vbCrLf)
End Sub))
' Execute two readers, one to read from first to last and the second from last to first.
For ctr As Integer = 0 To 1
Dim flag As Integer = ctr
tasks.Add(Task.Run( Sub()
Dim start, last, stp As Integer
Dim items As Integer
Do
Dim output As String = String.Empty
items = sc.Count
If flag = 0 Then
start = 1 : stp = 1 : last = items
Else
start = items : stp = -1 : last = 1
End If
For index As Integer = start To last Step stp
output += String.Format("[{0}] ", sc.Read(index))
Next
Console.WriteLine("Task {0} read {1} items: {2}{3}",
Task.CurrentId, items, output,
vbCrLf)
Loop While items < itemsWritten Or itemsWritten = 0
End Sub))
Next
' Execute a red/update task.
tasks.Add(Task.Run( Sub()
For ctr As Integer = 1 To sc.Count
Dim value As String = sc.Read(ctr)
If value = "cucumber" Then
If sc.AddOrUpdate(ctr, "green bean") <> SynchronizedCache.AddOrUpdateStatus.Unchanged Then
Console.WriteLine("Changed 'cucumber' to 'green bean'")
End If
End If
Next
End Sub ))
' Wait for all three tasks to complete.
Task.WaitAll(tasks.ToArray())
' Display the final contents of the cache.
Console.WriteLine()
Console.WriteLine("Values in synchronized cache: ")
For ctr As Integer = 1 To sc.Count
Console.WriteLine(" {0}: {1}", ctr, sc.Read(ctr))
Next
End Sub
End Module
' The example displays output like the following:
' Task 1 read 0 items:
'
' Task 3 wrote 17 items
'
' Task 1 read 17 items: [broccoli] [cauliflower] [carrot] [sorrel] [baby turnip] [
' beet] [brussel sprout] [cabbage] [plantain] [spinach] [grape leaves] [lime leave
' s] [corn] [radish] [cucumber] [raddichio] [lima beans]
'
' Task 2 read 0 items:
'
' Task 2 read 17 items: [lima beans] [raddichio] [cucumber] [radish] [corn] [lime
' leaves] [grape leaves] [spinach] [plantain] [cabbage] [brussel sprout] [beet] [b
' aby turnip] [sorrel] [carrot] [cauliflower] [broccoli]
'
' Changed 'cucumber' to 'green bean'
'
' Values in synchronized cache:
' 1: broccoli
' 2: cauliflower
' 3: carrot
' 4: sorrel
' 5: baby turnip
' 6: beet
' 7: brussel sprout
' 8: cabbage
' 9: plantain
' 10: spinach
' 11: grape leaves
' 12: lime leaves
' 13: corn
' 14: radish
' 15: green bean
' 16: raddichio
' 17: lima beans