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,不允许递归。 对于所有新开发,建议使用此默认策略,因为递归引入了不必要的复杂性,并使代码更容易出现死锁。 若要简化从使用MonitorReaderWriterLock的现有项目的迁移,也可以使用LockRecursionPolicy.SupportsRecursion标志创建允许递归的ReaderWriterLockSlim实例。
线程可以进入三种模式的锁:读取模式、写入模式和可升级的读取模式。 (在本主题的其余部分,“可升级读取模式”称为“可升级模式”,而短语“Enter x
mode”则优先于较长短语“进入模式”。 x
)
无论递归策略如何,随时只能有一个线程处于写入模式。 当线程处于写入模式时,任何其他线程都不能在任何模式下进入锁。 随时只能有一个线程处于可升级模式。 任意数量的线程都可以处于读取模式,并且其他线程处于读取模式时,可能有一个线程处于可升级模式。
重要
此类型实现 IDisposable 接口。 在使用完类型后,您应直接或间接释放类型。 若要直接释放类型,请在 try
/catch
块中调用其 Dispose 方法。 若要间接释放类型,请使用 using
(在 C# 中)或 Using
(在 Visual Basic 中)等语言构造。 有关详细信息,请参阅 IDisposable 接口主题中的“使用实现 IDisposable 的对象”一节。
ReaderWriterLockSlim 具有托管线程相关性;也就是说,每个 Thread 对象必须进行自己的方法调用才能进入和退出锁定模式。 没有线程可以更改另一个线程的模式。
ReaderWriterLockSlim如果不允许递归,则尝试输入锁的线程可能会因多种原因而阻止:
如果存在等待进入写入模式的线程,或者写入模式下有单个线程,则尝试进入读取模式的线程会阻止。
注意
当编写器排队时阻止新读者是有利于编写器的锁定公平性策略。 目前的公平性政策平衡了读者和作家的公平性,以促进最常见的方案中的吞吐量。 .NET 的未来版本可能会引入新的公平性策略。
如果已有线程处于可升级模式,或者存在等待进入写入模式的线程,或者写入模式下存在单个线程,则尝试进入可升级模式的线程会阻止该线程。
如果三种模式中有一个线程,则尝试进入写入模式的线程会阻止。
升级和降级锁
可升级模式适用于线程通常从受保护资源读取的情况,但如果满足某些条件,则可能需要写入该模式。 进入可升级模式的线程 ReaderWriterLockSlim 具有对受保护资源的读取访问权限,并且可以通过调用 EnterWriteLock 或 TryEnterWriteLock 方法升级到写入模式。 由于一次只能有一个线程处于可升级模式,因此不允许递归时升级到写入模式不能死锁,这是默认策略。
重要
无论递归策略如何,最初进入读取模式的线程都不允许升级到可升级模式或写入模式,因为该模式会产生强烈的死锁概率。 例如,如果两个处于读取模式的线程都尝试进入写入模式,则它们将死锁。 可升级模式旨在避免此类死锁。
如果读取模式下还有其他线程,则升级的线程将阻止。 当线程被阻止时,会阻止尝试进入读取模式的其他线程。 当所有线程都退出读取模式时,阻止的可升级线程进入写入模式。 如果还有其他线程等待进入写入模式,它们将保持阻塞状态,因为处于可升级模式的单线程会阻止它们获得对资源的独占访问权限。
当处于可升级模式的线程退出写入模式时,等待进入读取模式的其他线程可以执行此操作,除非有线程等待进入写入模式。 可升级模式下的线程可以无限期升级和降级,只要它是写入受保护资源的唯一线程。
重要
如果允许多个线程进入写入模式或可升级模式,则不得允许一个线程垄断可升级模式。 否则,尝试直接进入写入模式的线程将被无限期阻止,当它们被阻止时,其他线程将无法进入读取模式。
可升级模式下的线程可以先调用 EnterReadLock 该方法,然后调用 ExitUpgradeableReadLock 该方法,将读取模式降级为读取模式。 即使所有锁递归策略 NoRecursion都允许使用此降级模式。
降级到读取模式后,线程在从读取模式退出之前无法重新进入可升级模式。
以递归方式输入锁
可以使用指定锁策略和指定LockRecursionPolicy.SupportsRecursion锁策略的ReaderWriterLockSlim(LockRecursionPolicy)构造函数创建ReaderWriterLockSlim支持递归锁项。
注意
不建议对新开发使用递归,因为它引入了不必要的复杂性,并使代码更容易出现死锁。
对于允许递归的模式 ReaderWriterLockSlim ,可以考虑线程可以输入的模式:
读取模式下的线程可以递归进入读取模式,但不能进入写入模式或可升级模式。 如果尝试执行此操作,则会引发 a LockRecursionException 。 进入读取模式,然后进入写入模式或可升级模式是具有强死锁概率的模式,因此不允许这样做。 如前所述,为需要升级锁的情况提供可升级模式。
可升级模式下的线程可以进入写入模式和/或读取模式,并且可以以递归方式进入这三种模式中的任何一种。 但是,如果读取模式下存在其他线程,则尝试进入写入模式块。
写入模式下的线程可以进入读取模式和/或可升级模式,并且可以以递归方式进入这三种模式中的任何一种。
未进入锁的线程可以进入任何模式。 此尝试可能会因尝试输入非递归锁而阻止。
只要线程退出进入该模式的次数,线程就可以退出其按任意顺序输入的模式。 如果线程尝试退出模式的次数过多,或者退出未进入的模式,则会引发 a SynchronizationLockException 。
锁定状态
你可能会发现,根据锁的状态来考虑锁会很有用。 A ReaderWriterLockSlim 可以是以下四种状态之一:未输入、读取、升级和写入。
未输入:在此状态下,没有线程进入锁(或所有线程都已退出锁)。
读取:在此状态下,一个或多个线程已输入锁以读取对受保护资源的访问权限。
注意
线程可以使用或TryEnterReadLock方法或从可升级模式降级,以读取模式EnterReadLock进入锁定。
升级:在此状态下,一个线程已输入用于读取访问的锁,并可以选择升级到写入访问(即处于可升级模式),零个或多个线程已进入读取访问锁。 一次只能有一个线程输入锁,并可以选择升级;尝试进入可升级模式的其他线程将被阻止。
写入:在此状态下,一个线程已进入锁定,以便对受保护资源进行写入访问。 该线程拥有锁的独占性。 出于任何原因尝试输入锁的任何其他线程都将被阻止。
下表描述了在线程 t
执行最左侧列中所述的操作时,不允许递归的锁状态之间的转换。 在执行操作时, t
没有模式。 (表脚注中描述了处于可升级模式的特殊情况 t
。顶部行描述锁的起始状态。 单元格描述线程发生的情况,并显示对括号中的锁定状态的更改。
切换 | 未输入 (N) | 读取 (R) | 升级 (U) | 写入 (W) |
---|---|---|---|---|
t 进入读取模式 |
t 输入 (R)。 |
t 如果线程正在等待写入模式,则阻止;否则, t 输入。 |
t 如果线程正在等待写入模式,则阻止;否则, t 输入。1 |
t 块。 |
t 进入可升级模式 |
t enters (U)。 |
t 如果线程正在等待写入模式或升级模式,则阻止;否则, t 输入 (U)。 |
t 块。 |
t 块。 |
t 进入写入模式 |
t enters (W)。 |
t 块。 |
t 块。2 |
t 块。 |
1 如果 t
以可升级模式启动,则进入读取模式。 此操作永远不会阻止。 锁状态不会更改。 (然后,线程可以通过退出可升级模式完成降级到读取模式。
2 如果 t
以可升级模式启动,则它会阻止读取模式下的线程。 否则,它会升级到写入模式。 锁定状态更改为“写入”(W)。 如果 t
由于读取模式下存在线程而阻止,则只要最后一个线程退出读取模式,它就会进入写入模式,即使有等待进入写入模式的线程也是如此。
当由于线程退出锁而发生状态更改时,将按如下所示选择要唤醒的下一个线程:
- 首先,正在等待写入模式且已处于可升级模式的线程(最多可以有一个这样的线程)。
- 失败,正在等待写入模式的线程。
- 失败,正在等待可升级模式的线程。
- 失败,正在等待读取模式的所有线程。
在第三种情况下,锁的后续状态始终为写入(W),在第三种情况下升级(U),而不管退出线程触发状态更改时锁的状态如何。 在最后一种情况下,如果状态更改后有线程处于可升级模式,则锁的状态为 Upgrade (U),否则,无论以前的状态如何。
示例
下面的示例演示了一个简单的同步缓存,该缓存保存包含整数键的字符串。 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