При каких условиях TryDequeue и подобные методы System.Collections.Concurrent не работают
Недавно я заметил, что внутри объектов коллекции, содержащихся в пространстве имен System.Collections.Concurrent, обычно встречается Collection.TrySomeAction()
, а не Collection.SomeAction()
.
В чем причина этого? Я предполагаю, что это имеет какое-то отношение к блокировке?
Итак, я задаюсь вопросом, при каких условиях можно попытаться (например) удалить объект из стека, очереди, сумки и т.д..
Ответы
Ответ 1
Коллекции в пространстве имен System.Collections.Concurrent
считаются потокобезопасными, поэтому их можно использовать для написания многопоточных программ, которые обмениваются данными между потоками.
До .NET 4 вам пришлось предоставить свои собственные механизмы синхронизации, если несколько потоков могут иметь доступ к одной общей коллекции. Вы должны были блокировать коллекцию каждый раз, когда вы изменяли ее элементы. Вам также может потребоваться блокировка коллекции каждый раз, когда вы обращаетесь к ней (или перечисляете ее). Это для простейших многопоточных сценариев. В некоторых приложениях создаются фоновые потоки, которые с течением времени доставляют результаты в общую коллекцию. Другой поток будет читать и обрабатывать эти результаты. Вам нужно было реализовать собственную схему передачи сообщений между потоками, чтобы уведомлять друг друга, когда были доступны новые результаты, и когда эти новые результаты были использованы. Классы и интерфейсы в System.Collections.Concurrent
обеспечивают согласованную реализацию для тех и других общих задач многопоточного программирования, связанных с совместными данными через потоки, в режиме блокировки.
Try<something>
имеет семантику - попробуйте выполнить это действие и вернуть результат операции. Семантика DoThat
обычно использует исключенную механику для указания ошибки, которая может быть неэффективной. В качестве примеров они могут возвращать false,
- Если вы попытаетесь добавить новый элемент, возможно, у вас уже есть его в
ConcurentDictionary
;
- если вы попытаетесь получить элемент из коллекции, он может там не существовать;
- Если вы попытаетесь обновить элемент, то может быть более поздний элемент уже, поэтому метод гарантирует, что он обновит только элемент, который должен был обновиться.
Попробуйте прочитать:
Ответ 2
Что вы имеете в виду с ошибкой?
Возьмем следующий пример:
var queue = new Queue<string>();
string temp = queue.Dequeue();
// do something with temp
Вышеприведенный код генерирует исключение, поскольку мы пытаемся удалить из пустой очереди. Теперь, если вместо этого вы используете ConcurrentQueue<T>
:
var queue = new ConcurrentQueue<string>();
string temp;
if (queue.TryDequeue(out temp))
{
// do something with temp
}
Вышеприведенный код не будет генерировать исключение. Очередь все равно не удалит элемент, но код не будет сбой при вызове исключения. Реальное использование для этого становится очевидным в многопоточной среде. Код для неконкурентного Queue<T>
обычно выглядит примерно так:
lock (queueLock)
{
if (queue.Count > 0)
{
string temp = queue.Dequeue();
// do something with temp
}
}
Чтобы избежать условий гонки, нам нужно использовать блокировку, чтобы гарантировать, что ничего не происходит с очередью за время, прошедшее с проверки Count
, вызывающей Dequeue
. С ConcurrentQueue<T>
нам действительно не нужно проверять Count
, но вместо этого можно называть TryDequeue
.
Если вы исследуете типы, найденные в пространстве имен Systems.Collections.Concurrent
, вы обнаружите, что многие из них переносят две операции, которые обычно называются последовательно, и традиционно требуется блокировка (Count
, а затем Dequeue
в ConcurrentQueue<T>
, GetOrAdd
в ConcurrentDictionary<TKey, TValue>
заменяет последовательности вызова ContainsKey
, добавляет элемент и получает его и т.д.).
Ответ 3
Если нет ничего, что можно было бы "отменить", например... Этот "Try-Pattern" обычно используется во всех элементах FCL и BCL. Это не имеет ничего общего с блокировкой, параллельные коллекции (или, по крайней мере, должны быть) в основном реализованы без блокировок...