System.Threading.ReaderWriterLockSlim 類別
本文提供此 API 參考文件的補充備註。
使用 ReaderWriterLockSlim 來保護多個線程讀取的資源,並一次由一個線程寫入。 ReaderWriterLockSlim 允許多個線程處於讀取模式,允許一個線程處於具有鎖定獨佔擁有權的寫入模式,並允許具有可升級讀取存取權的線程進入可升級的讀取模式,線程可以升級至寫入模式,而不需要放棄對資源的讀取存取權。
注意
- ReaderWriterLockSlim 類似於 ReaderWriterLock,但是它有遞迴以及升級和降級鎖定狀態的簡化規則。 ReaderWriterLockSlim 可避免可能發生死結的許多情況。 此外,ReaderWriterLockSlim 的效能明顯優於 ReaderWriterLock。 建議針對所有新的開發使用 ReaderWriterLockSlim。
- ReaderWriterLockSlim 不是線程中止安全。 您不應該在可中止存取線程的環境中使用它,例如 .NET Framework。 如果您使用 .NET Core 或 .NET 5+,則應該沒問題。 Abort .NET Core 不支援 ,而且 在 .NET 5 和更新版本中已過時 。
根據預設,會使用 LockRecursionPolicy.NoRecursion 旗標建立 的新實例ReaderWriterLockSlim,而且不允許遞歸。 建議針對所有新的開發使用此預設原則,因為遞歸會造成不必要的複雜問題,並讓您的程式代碼更容易發生死結。 若要簡化從使用 Monitor 或 ReaderWriterLock的現有項目移轉,您可以使用 LockRecursionPolicy.SupportsRecursion 旗標建立允許遞歸的 ReaderWriterLockSlim 實例。
線程可以進入三種模式的鎖定:讀取模式、寫入模式和可升級的讀取模式。 (在本主題的其餘部分,「可升級的讀取模式」稱為「可升級模式」,而「進入 x
模式」片語則優先使用片語「進入模式」 x
。
不論遞歸原則為何,隨時只能有一個線程處於寫入模式。 當線程處於寫入模式時,沒有任何其他線程可以在任何模式中進入鎖定。 隨時只能有一個線程處於可升級模式。 任意數目的線程都可以處於讀取模式,而且在可升級模式中可以有一個線程,而其他線程則處於讀取模式。
重要
此型別代表 IDisposable 介面。 當您完成使用型別時,您應該直接或間接處置它。 若要直接處置型別,請呼叫其 try
/catch
區塊中的 Dispose 方法。 若要間接處置它,請使用語言建構函式,例如 using
(在 C# 中) 或 Using
(在 Visual Basic 中)。 如需詳細資訊,請參閱 IDisposable 介面文章中的<使用實作 IDisposable 的物件>一節。
ReaderWriterLockSlim 具有 Managed 線程親和性;也就是說,每個 Thread 對象都必須進行自己的方法呼叫,才能進入和結束鎖定模式。 沒有線程可以變更另一個線程的模式。
ReaderWriterLockSlim如果 不允許遞歸,嘗試進入鎖定的線程可能會因為數個原因而封鎖:
如果有線程正在等候進入寫入模式,或寫入模式中有單個線程,則嘗試進入讀取模式的線程會封鎖。
注意
當寫入器排入佇列時,封鎖新的讀取器是有利於寫入器的鎖定公平性原則。 目前的公平性政策平衡了讀者和作家的公平性,以在最常見的案例中促進輸送量。 未來的 .NET 版本可能會引入新的公平性原則。
如果已經有可升級模式的線程、有等候進入寫入模式的線程,或寫入模式中有單個線程,則嘗試進入可升級模式的線程會封鎖。
如果三種模式中有一個線程,則嘗試進入寫入模式區塊的線程。
升級和降級鎖定
可升級模式適用於線程通常會從受保護的資源讀取的情況,但如果符合某些條件,可能需要寫入該模式。 在可升級模式中進入 ReaderWriterLockSlim 的線程具有受保護資源的讀取許可權,而且可以藉由呼叫 EnterWriteLock 或 TryEnterWriteLock 方法升級至寫入模式。 因為一次只能有一個線程處於可升級模式,所以不允許遞歸時升級至寫入模式無法死結,這是默認原則。
重要
不論遞歸原則為何,一開始進入讀取模式的線程都不允許升級為可升級模式或寫入模式,因為該模式會產生強機率的死結。 例如,如果讀取模式中的兩個線程都嘗試進入寫入模式,它們就會死結。 可升級模式的設計是為了避免這類死結。
如果讀取模式中有其他線程,則正在升級的線程會封鎖。 當線程遭到封鎖時,會封鎖嘗試進入讀取模式的其他線程。 當所有線程都結束讀取模式時,封鎖的可升級線程會進入寫入模式。 如果有其他線程等待進入寫入模式,它們仍會遭到封鎖,因為處於可升級模式的單一線程會防止它們取得資源的獨佔存取權。
當可升級模式中的線程結束寫入模式時,除非有線程等候進入寫入模式,否則等候進入讀取模式的其他線程可以執行此動作。 可升級模式中的線程可以無限期升級和降級,只要它是寫入受保護資源的唯一線程。
重要
如果您允許多個線程進入寫入模式或可升級模式,則不得允許一個線程壟斷可升級模式。 否則,嘗試直接進入寫入模式的線程將會無限期封鎖,而當線程遭到封鎖時,其他線程將無法進入讀取模式。
可升級模式中的線程可以先呼叫 EnterReadLock 方法,然後呼叫 ExitUpgradeableReadLock 方法,降級為讀取模式。 所有鎖定遞歸原則都允許此降級模式,即使是 NoRecursion。
降級至讀取模式之後,線程在結束讀取模式之前,無法重新進入可升級模式。
以遞歸方式輸入鎖定
您可以使用指定鎖定原則的建構函式,以及指定 ,建立ReaderWriterLockSlim支援遞歸鎖定專案的 ReaderWriterLockSlim(LockRecursionPolicy) 。LockRecursionPolicy.SupportsRecursion
注意
不建議針對新的開發使用遞歸,因為它引進不必要的複雜問題,並讓您的程式代碼更容易發生死結。
ReaderWriterLockSlim針對允許遞迴的 ,可以針對線程可以輸入的模式說出下列內容:
讀取模式中的線程可以遞歸進入讀取模式,但無法進入寫入模式或可升級模式。 如果嘗試這樣做, LockRecursionException 則會擲回 。 進入讀取模式,然後進入寫入模式或可升級模式是具有強機率死結的模式,因此不允許。 如先前所述,針對需要升級鎖定的情況,會提供可升級模式。
可升級模式的線程可以進入寫入模式和/或讀取模式,而且可以遞歸地輸入這三種模式中的任何一種。 不過,如果讀取模式中有其他線程,嘗試進入寫入模式區塊。
寫入模式中的線程可以進入讀取模式和/或可升級模式,而且可以遞歸地輸入這三種模式中的任何一種。
未進入鎖定的線程可以進入任何模式。 此嘗試可能會因為嘗試輸入非遞歸鎖定而遭到封鎖的原因相同。
線程可以結束它以任何順序輸入的模式,只要它以完全相同的次數結束每個模式就進入該模式。 如果線程嘗試結束模式太多次,或結束尚未進入的模式, SynchronizationLockException 則會擲回 。
鎖定狀態
您可能會發現,就其狀態而言,考慮鎖定很有用。 ReaderWriterLockSlim可以是四種狀態之一:未輸入、讀取、升級和寫入。
未輸入:在此狀態下,沒有線程進入鎖定(或所有線程都已結束鎖定)。
讀取:在此狀態下,一或多個線程已進入鎖定以讀取受保護資源。
注意
線程可以使用 或 TryEnterReadLock 方法,或從可升級模式降級,進入讀取模式EnterReadLock中的鎖定。
升級:在此狀態下,一個線程已輸入讀取存取的鎖定,並可選擇升級為寫入存取權(也就是處於可升級模式),而零個或多個線程已進入讀取存取鎖定。 一次不能有一個以上的線程輸入鎖定,並可選擇升級;嘗試進入可升級模式的其他線程會遭到封鎖。
寫入:在此狀態下,一個線程已進入鎖定以寫入受保護資源。 該線程具有鎖定的獨佔擁有權。 因為任何原因而嘗試進入鎖定的任何其他線程會遭到封鎖。
下表描述鎖定狀態之間的轉換,當線程 t
採取最左邊數據行中所述的動作時,不允許遞歸的鎖定。 在採取動作時, t
沒有模式。 (在數據表腳註中描述處於可升級模式的特殊案例 t
。頂端數據列描述鎖定的開始狀態。 單元格會描述線程會發生什麼事,並顯示括弧中鎖定狀態的變更。
轉換 | 未輸入 (N) | 讀取 (R) | 升級 (U) | 寫入 (W) |
---|---|---|---|---|
t 進入讀取模式 |
t enters (R)。 |
t 如果線程正在等候寫入模式,則會封鎖;否則, t 輸入 。 |
t 如果線程正在等候寫入模式,則會封鎖;否則, t 輸入 。1 |
t 塊。 |
t 進入可升級模式 |
t enters (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 {0} wrote {1} items\n",
Task.CurrentId, itemsWritten);
} ));
// 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 {0} read {1} items: {2}\n",
Task.CurrentId, items, output);
} 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(" {0}: {1}", 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