Почему у 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, и вы знаете, чтобы повторить попытку, когда очередь свободна.

Другие вопросы по тегам