www.fgks.org   »   [go: up one dir, main page]

Как стать автором
Обновить

Самый простой и подробный гайд по конкурентным коллекциям в C#

Уровень сложностиПростой
Время на прочтение18 мин
Количество просмотров20K
Всего голосов 43: ↑41 и ↓2+60
Комментарии26

Комментарии 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, то просто изменится вероятность получения исключения

Если в примере автора с concurrent поменять tryAdd на первоначальную Add, то пример тут же вылетит. Итого: в корне неверный пример и объяснение.

И вообще, почему мы говорим "если"? Пример такой, какой он есть - неверный.

Верно, но я имел в виду что приложение именно не падает)

Согласен. Крайне неудачный, даже неправильный пример в статье и неправильное объяснение. То есть, это просто вредная информация. Далее..

И всякий раз при получении какого-либо элемента из этой коллекции будет вытаскиваться реальный тип элемента. Это означает, что упаковка и распаковка не требуются.

Опять неверно. Упаковка/распаковка в не-generic (старых) коллекциях делается только для value types.

Дальше читать не стал, слишком много ошибок, чтобы хоть насколько-то доверять этому тексту.

В вашем бенчмарке ещё не хватает обычного Dictionary. Было бы интересно сравнить ещё и с ним.

Они ни разу не "конкурентные". Concurrent в переводе с английского языка значит одновременный, согласованный, совпадающий. Обычно, их называют "параллельными" (в том числе официальный машинный перевод), но никак не "конкурентными".

Уже замучился спорить с этими конкурентниками.

Далее, в C# 2.0

Может в .net 2.0? Версии языка отражаются только на синтаксис, а никак не на библиотеках типов, которые так то никто не мешает хоть из бейсика использовать

Принципиальной ошибки нет, в .NET Framework 2 появились дженерики, их поддержка появилась в C# 2

.net это не только c#, так что это все таки ошибка, а для "глаз эксперта" так даже имхо довольно грубая

Стоило бы еще упомянуть о 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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий