Комментарии 15
А зачем volatile в примере? Imetrlocked.compareexchange это memory barrier, операции нельзя реордерить через барьер.
Вы даёте ученикам опасный инструмент, не давая никаких объяснений как работает memory model. Зачем-то учите их делать аналоги стандартной либы без рассказа о том, что она вообще есть. Это многое говорит о качестве вашего образования.
Вместо этого полезнее научить Джуна: используй Concurrent коллекции, профилируй!
И как в concurrent коллекциях решить проблему состояния гонки при доступе к нескольким коллекциям? Concurrent коллекции это мусор, писал об этом в той же статье из первого комментария.
Когда один поток захватывает блокировку, все остальные просто стоят в ожидании, пока он её отпустит
Это если используется блокирующая синхронизация. Советую почитать про неблокирующую.
И как в случае с interlocked решаются проблемы состояния гонки, особенно если нужно работать с несколькими коллекциями? Никак.
Вот тут и приходят на помощь lock-free структуры данных, которые позволяют нам обрабатывать данные в многозадачной среде без необходимости блокировать потоки. В их основе лежат атомарные операции, такие как CAS.
боже, как все запущено...
а знает ли уважаемый автор, что "обычные синхронизации типа lock, Monitor или Mutex" используют под капотом те же самые "атомарные операции, такие как CAS"?
известно ли уважаемому автору ПОЧЕМУ "обычные синхронизации" после некоторой долбежки в цикле просят помощи планировщика ОС??
ну и совсем уже подлый вопрос: ПОНЯТНО ЛИ автору, почему CAS не имеет смысла на однопроцессорной арихитектуре?!
ЗЫ читайте David R. Butenhof, если и правда хотите понять. а если нет времени, то просто ЗАДУМАЙТЕСЬ над фразой "Multithreading is defined by an application design that ALLOWS FOR concurrent or simultaneous execution".
ЗЗЫ лично я лет 15 назад задумался, и мои примеры производительности C++ на 8 потоках работали в 321 раз быстрее!
еще раз для тех, кто не понял: в ТРИСТА ДВАДЦАТЬ ОДИН РАЗ.
вот здесь статья: https://ders.by/cpp/mtprog/mtprog.html#3.1.1
а знает ли уважаемый автор, что "обычные синхронизации типа lock, Monitor или Mutex"
Не совсем коррекное утверждение - lock это лишь сахар для Monitor, а Monitor это довольно сложный алгоритм синхронизации, который включает и CAS и синхронизацию уровня ядра(Mutex) и диспечирезацию потоков(решения проблемы голодания) без перехода в ядро ОС.
ну и совсем уже подлый вопрос: ПОНЯТНО ЛИ автору, почему CAS не имеет смысла на однопроцессорной арихитектуре?!
Почему не имеет. Как насчёт барьера памяти и понятной семантики.
разберитесь чем concurrency отличается от parallelism.
Да вроде разбираюсь. И мой комментарий не затрагивает обе концепции. Похоже это вы не понимаете.
ЗЗЫ лично я лет 15 назад задумался, и мои примеры производительности C++ на 8 потоках работали в 321 раз быстрее!
еще раз для тех, кто не понял: в ТРИСТА ДВАДЦАТЬ ОДИН РАЗ.
Ваше утверждение нарушает закон Амдала.
В статье упоминается Thread.Volatile (которого не существует) вместо Thread.VolatileRead/VolatileWrite или Volatile.Read/Write, однако в коде используется ключевое слово volatile. Это как бы не совсем одно и то же.
Ну и CAS совсем не бесплатный, такая реализация lock-free может быть медленнее при некоторых профилях нагрузки чем Monitor. Надо иногда хотя бы отпускать поток, иначе будете просто греть воздух в датацентре, SpinWait хотя бы...
Нужны бенчмарки, причём для разного уровня конкурентности, с разными профилями нагрузки (пропорции Push/Pop для стека например).
А есть где-то исчерпывающее объяснение, что делаетvolatile в современном .NET и для разных архитектур процессоров т.к. у нас уже есть ARM помимо x86
Memory model для ARM не особо отличается в .NET, они затянули рантайме самые неприятные места, воткнув барьеры где надо (типа конструкторов). В .NET вообще достаточно жёсткая модель, это не C/C++, особенно на 64-битных системах где почти все используемые типы читаются и пишутся атомарно, если не вмешиваться в выравнивание и не делать p/invoke.
Хорошая статья (серия статей) про модели памяти: https://preshing.com/20120913/acquire-and-release-semantics/
Пример с Version полностью некорректен.
Во-первых, поле Version заполняется, но нигде не проверяется, то есть оно бесполезное.
Во-вторых, в качестве "версии" используется по сути "глубина стека" у текущего элемента
newNode.Version = oldHead?.Version + 1 ?? 0;
То есть, ABA проблема не решается. Пока наш поток собирается положить элемент, другой поток успел снять N элементов и положить те же N - текущая глубина не изменится (версия та же), а ноды в стеке другие.

Как создавать lock-free структуры данных в C# на базе CAS и Thread.Volatile