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