Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье приводятся дополнительные замечания к справочной документации по этому 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 интерфейс. Завершив использование типа, следует избавиться от него напрямую или косвенно. Чтобы удалить тип напрямую, вызовите его Dispose метод в блоке try
/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 выбрасывается исключение. Ввод режима чтения, а затем ввод режима записи или обновляемого режима является шаблоном с высокой вероятностью взаимоблокировок, поэтому это не допускается. Как упоминалось ранее, режим обновления предоставляется для случаев, когда необходимо обновить блокировку.
Поток в обновляемом режиме может входить в режим записи и (или) режим чтения и может ввести любой из трех режимов рекурсивно. Однако попытка войти в режим записи блокируется, если в режиме чтения находятся другие потоки.
Поток в режиме записи может входить в режим чтения и (или) обновляемый режим и может вводить любой из трех режимов рекурсивно.
Поток, который не вошел в зону блокировки, может войти в любой режим. Эта попытка может блокироваться по тем же причинам, что и попытка ввести не рекурсивную блокировку.
Поток может выйти из режимов, введенных в любом порядке, если он выходит из каждого режима ровно столько раз, сколько он вошел в этот режим. Если поток пытается выйти из режима слишком много раз или выйти из режима, в который он не входил, создается 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) в третьем случае, независимо от состояния блокировки в момент, когда поток, завершивший работу, вызвал изменение состояния. В последнем случае состояние блокировки — обновление (U), если после изменения состояния поток находится в режиме, поддерживающем обновление, а в противном случае — чтение (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
первый записывает имена овощей, хранящихся в массиве. Вторая и третья задача отображают имена овощей, первый в порядке возрастания (от низкого индекса до высокого индекса), второй в порядке убывания. Последняя задача ищет строку "огурец" и, когда она находит его, вызывает EnterUpgradeableReadLock метод для замены строки "зеленый боб".
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