Почему у ConcurrentQueue и ConcurrentDictionary есть методы "Try" - TryAdd, TryDequeue - вместо Add и Dequeue?
ConcurrentQueue
имеет TryDequeue
метод.
Queue
только что Dequeue
метод.
В ConcurrentDictionary
здесь нет Add
метод, но у нас есть TryAdd
вместо.
Мой вопрос:
В чем разница между этими параллельными методами сбора? Почему они разные для одновременных коллекций?
5 ответов
С Dictionary<TKey, TValue>
предполагается, что вы собираетесь реализовать свою собственную логику, чтобы убедиться, что дублирующиеся ключи не введены. Например,
if(!myDictionary.ContainsKey(key)) myDictionary.Add(key, value);
Но мы используем параллельные коллекции, когда у нас работает несколько потоков, и возможно, что они оба пытаются изменить словарь одновременно.
Если два потока пытались выполнить вышеуказанный код одновременно, возможно, что myDictionary.ContainsKey(key)
может вернуть false для обоих потоков, потому что они оба проверяют одновременно, и этот ключ еще не был добавлен. Затем они оба пытаются добавить ключ, и один не удается.
Тот, кто читает этот код и не знает, что он многопоточный, может быть сбит с толку. Я проверил, чтобы убедиться, что ключ не был в словаре, прежде чем я добавил его. Так как я получаю исключение?
ConcurrentDictionary.TryAdd
решает это, позволяя вам "попытаться" добавить ключ. Если он добавляет значение, он возвращает true
, Если это не так, это возвращает false
, Но то, что он не сделает, это конфликт с другим TryAdd
и бросить исключение.
Вы можете сделать все это самостоятельно, завернув Dictionary
в классе и положить lock
заявления вокруг него, чтобы убедиться, что только один поток за раз вносит изменения. ConcurrentDictionary
просто делает это для вас и делает это действительно хорошо. Вам не нужно видеть все детали того, как он работает - вы просто используете его, зная, что многопоточность была учтена.
Вот деталь, которую нужно искать при использовании класса в многопоточном приложении. Если вы перейдете к документации для класса ConcurrentDictionary и прокрутите вниз, вы увидите это:
Поток безопасности
Все открытые и защищенные члены ConcurrentDictionary являются поточно-ориентированными и могут использоваться одновременно из нескольких потоков. Однако члены, доступ к которым осуществляется через один из интерфейсов, которые реализует ConcurrentDictionary, включая методы расширения, не гарантируют поточнобезопасность и могут потребовать синхронизации вызывающей стороной.
Другими словами, несколько потоков могут безопасно читать и изменять коллекцию.
Под Словарным классом вы увидите это:
Поток безопасности
Словарь может поддерживать несколько читателей одновременно, если коллекция не изменена. Тем не менее, перечисление в коллекции по сути не является потокобезопасной процедурой. В редком случае, когда перечисление конкурирует с доступом для записи, коллекция должна быть заблокирована в течение всего перечисления. Чтобы доступ к коллекции был доступен нескольким потокам для чтения и записи, необходимо реализовать собственную синхронизацию.
Несколько потоков могут читать ключи, но если несколько потоков собираются писать, то вам нужно как-то lock
словарь, чтобы убедиться, что только один поток одновременно пытается обновить.
Dictionary<TKey, TValue>
подвергает Keys
коллекция и Values
коллекции, так что вы можете перечислять ключи и значения, но он предупреждает вас не пытаться делать это, если другой поток будет изменять словарь. Вы не можете перечислить что-то, когда элементы добавляются или удаляются. Если вам нужно перебирать ключи или значения, вам нужно заблокировать словарь, чтобы предотвратить обновления во время этой итерации.
ConcurrentDictionary<TKey, TValue>
Предполагается, что чтение и запись будут выполняться несколькими потоками, поэтому он даже не предоставляет набор ключей или значений для перечисления.
Причина, по которой эти методы Try
семантика заключается в том, что по замыслу, нет никакого способа надежно сказать, что Dequeue
или же Add
операции будут успешными.
Когда очередь не параллельна, вы можете проверить, есть ли что-нибудь, чтобы удалить из очереди, прежде чем вызывать Dequeue
метод. Точно так же вы можете проверить, если ключ в Dictionary
присутствует или нет. Вы не можете сделать то же самое для параллельных классов, потому что кто-то может снять ваш элемент с очереди после того, как вы проверили его наличие, но до того, как вы действительно его удалите. Другими словами, Try
Операции позволяют проверить предварительное условие и выполнить операцию атомарно.
Альтернативный подход позволил бы вам в любом случае разрешить удаление из очереди или добавление и выдать исключение в случае сбоя операции, как это делают непоследовательные реализации. Недостатком этого подхода является то, что эти исключительные ситуации в неконкурентных классах полностью ожидаются в параллельных классах, поэтому использование обработки исключений для них было бы неправильным.
Семантика другая.
Отсутствие Queue.Dequeue
обычно указывает на проблему с внутренней логикой приложения, поэтому в таких случаях полезно создавать исключения.
Однако провал ConcurrentQueue.TryDeque
это то, что можно ожидать в обычном потоке, поэтому избегая исключения и возвращая Boolean
это разумный способ справиться с этим.
ConcurrentQueue<T>
обрабатывает всю синхронизацию внутри. Если две темы называютTryDequeue
в один и тот же момент ни одна из операций не блокируется. Когда обнаружен конфликт между двумя потоками, один поток должен попытаться снова получить следующий элемент, и синхронизация обрабатывается внутри.
(Обычно в.NET Framework иметь Try...
функции, которые возвращают логические результаты вместо броска, см., например, TryParse
методы.)
Поскольку эти коллекции предназначены для одновременного использования, вы не можете полагаться на последовательную проверку предварительных условий, вам нужна атомарная операция.
Взяв словарь в качестве примера, обычно вы можете написать такой код:
if (!dictionary.ContainsKey(key))
{
dictionary.Add(key, value);
}
В ситуации, когда несколько потоков используют один и тот же словарь, вполне возможно, что другой поток вставит значение с тем же ключом между проверками ContainsKey
и звонит Add
,
TryAdd
решает это, так как он будет либо успешным, либо неудачным в зависимости от того, существует ключ или нет.
Из MSDN:
Пытается удалить и вернуть объект в начале параллельной очереди.
Возвращает
Значение true, если элемент был удален и успешно возвращен из начала ConcurrentQueue; иначе ложно.
Так что, если вы можете удалить TryDequeue
просто удалите и верните его, если не можете вернуть false, и вы знаете, чтобы повторить попытку, когда очередь свободна.