Комментарии 26
В первом примере дело же не в ConcurrentDictionary
а в методе TryAdd
вместо Add
. Вот такой код отработает без ошибок даже с обычным Dictionary
:
Dictionary<int, string> dictionary = []; // <- обычный dictionary
var t1 = new Thread(Method1);
var t2 = new Thread(Method2);
t1.Start(); // Запускаем
t2.Start();
t1.Join(); // Ждем
t2.Join();
foreach (var item in dictionary)
Console.WriteLine($"Key:{item.Key}, Value:{item.Value}");
void Method1()
{
for (var i = 0; i < 10; i++)
{
// В примере 1 тут сначала Add, потом TryAdd
dictionary.TryAdd(i, "Added By Method1 " + i);
Thread.Sleep(100);
}
}
void Method2()
{
for (var i = 0; i < 10; i++)
{
// Аналогично
dictionary.TryAdd(i, "Added By Method2 " + i);
Thread.Sleep(100);
}
}
Соответственно вывод программы точно такой-же как и в варианте с ConcurrentDictionary
:
Key:0, Value:Added By Method1 0
Key:1, Value:Added By Method2 1
Key:2, Value:Added By Method1 2
Key:2, Value:Added By Method2 2
Key:3, Value:Added By Method2 3
Key:4, Value:Added By Method2 4
Key:5, Value:Added By Method1 5
Key:6, Value:Added By Method1 6
Key:7, Value:Added By Method2 7
Key:8, Value:Added By Method2 8
Key:9, Value:Added By Method1 9
Чудес не бывает. Ничего не блокируется, а между проверкой наличия ключа и добавлением всегда есть вероятность, что кто-то это сделает быстрее. Не получится ли внутренняя ошибка?
Key:2, Value:Added By Method1 2
Key:2, Value:Added By Method2 2
И теперь у вас по одному ключу 2 разных значения. Работа со словарем превращается в русскую рулетку.
Не важно, во что она превращается. Важно, что объяснение в статье вообще неверное.
Зачем минусы? Автор написал, что пример падает по исключению из-за не потокобезопасной коллекции. Это в корне неверно. Автор коммента выше лишь показал, что с TryAdd пример не падает. Что и требовалось доказать. Хотя я бы наоборот, поменял с TryAdd на Add в примере с concurrent, и там бы всё тоже упало.
Не переживайте за минусы, это нормально на хабре. Я так вообще до сих пор использую .NET версии 4.0 и предпочитаю вручную создавать потоки и использовать примитивы синхронизации вместо async/await.
То что в примере кода выше не возникло исключение не означает что его не будет.
Ещё раз - сам первоначальный пример автора в корне неверный. В корне. А что написали в ответах, не так важно.
Если в примере автора поменять на tryadd, то просто изменится вероятность получения исключения
Верно, но я имел в виду что приложение именно не падает)
Согласен. Крайне неудачный, даже неправильный пример в статье и неправильное объяснение. То есть, это просто вредная информация. Далее..
И всякий раз при получении какого-либо элемента из этой коллекции будет вытаскиваться реальный тип элемента. Это означает, что упаковка и распаковка не требуются.
Опять неверно. Упаковка/распаковка в не-generic (старых) коллекциях делается только для value types.
Дальше читать не стал, слишком много ошибок, чтобы хоть насколько-то доверять этому тексту.
В вашем бенчмарке ещё не хватает обычного Dictionary. Было бы интересно сравнить ещё и с ним.
Они ни разу не "конкурентные". Concurrent в переводе с английского языка значит одновременный, согласованный, совпадающий. Обычно, их называют "параллельными" (в том числе официальный машинный перевод), но никак не "конкурентными".
Далее, в C# 2.0
Может в .net 2.0? Версии языка отражаются только на синтаксис, а никак не на библиотеках типов, которые так то никто не мешает хоть из бейсика использовать
Стоило бы еще упомянуть о System.Threading.Channels, которые отлично подходят для реализации producer-consumer
Для достижения потокобезопасности эти типы используют различные виды эффективных lock-free механизмов синхронизации. Что это за примитивы синхронизации? Обычно это вариации спинлоков с неблокирующим ожиданием. При таких примитивах синхронизации поток, пытающийся получить лок, ожидает в цикле, который раз за разом проверяет доступность этого лока.
сомнительное лок фри получается, если в цикле раз за разом пытаться проверять доступность лока)
Я бы как раз 10 раз подумал прежде чем использовать ConcurrentDictionary вместо Dictionary. Как раз из-за реализации. Использование механизмов lock поверх методов обычного Dictionary выигрывают в производительности по сравнению с lock free в ConcurrentDictionary. К тому же lock предоставляет больше контроля над процессом и ты точно знаешь, есть ли элемент в коллекции в определенный момент времени. Есть определенные ситуации с high concurrency, когда ConcurrentDictionary действительно будет лучше, но... их не очень много.
Верно, ConcurrentDictionary очень хорош для простых сценариев + не засоряет код. Но вот если ожидаются частые добавления (частые аллокации к памяти из-за захвата контекста) или комплексные условия, то своя реализация лока (с режимами read-write например) будет выйгрывать.
Про использование подобных коллекций - согласен, писал в том числе про это в статье про очереди.
До спойлера в статье вставлено PNG изображение размером более 1 МиБ.
TryPop(out T)
: пытается получить первый элемент.
Здесь ошибка. Stack отличается от Queue тем, что извлекается последний добавленный элемент, реализуя принцип LIFO
Самый простой и подробный гайд по конкурентным коллекциям в C#