Pull to refresh

Comments 36

Внезапно, при одновременной модификации структуры данных из параллельных процессов случились проблемы. Кто бы мог подумать.

Априори всё не thread-safe, пока в документации явно не сказано обратное.

Я уже комментировал эту статью в другом месте — в JDK чётко прописано, что эта структура без какой-либо синхронизации.

И HashMap тоже…

  1. Обращение по нулевому указателю С++ отлавливает, но в вашем примере его скорее всего просто не было. Параллельный доступ к дереву испортил его структуру, вероятно - часть памяти была утеряна, но обращения по нулевому указателю не было. "Испорченную" структуру данных типа зацикленного списка не отловит ничего, ну разве что кроме сторожевого таймера.

  2. Рекомендую попробовать ваш пример для С++ со включённым анализатором времени исполнения - например, clang AddressSanitizer может отловить всякое.

  3. Нет ни одной здравой причины перехватывать исключения, ну разве кроме случаев когда через исключение передают информацию о логической ошибке. Программа с ошибкой должна немедленно грохаться, чтобы эту ошибку можно было выявить и исправить. Максимум, что можно себе позволить: сохранить дополнительные данные в процессе завершения.

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

Программа программе рознь. Самолет не должен падать при поломке чайника.

Вот что удивительно бесполезный модификатор const есть, а модификатора single_thread, reinterant, thread_safe, pure... нет как и других явных указаний компилятору, что бы он мог предупреждать или не использовать оптимизации, исходя из не явных предположений, которые еще могут быть и не верны.

И тут из-за угла выглядывает Rust со своими Send и Sync.

не понимаю почему вирусится статья, нубская ошибка, все остальное словобудие

Если это не разбирать, то это уйдет в категорию магии. Почему нельзя шарить мапу между тредами? А вот нельзя и всё. Б-г накажет. Это будет хуже, чем нубство.

очевидно почему и он универсален для любой сложной структуры данных: там много разных логически связанных переменных, в многопоточной среде выполнения никто не знает какой набор значений в них будет и когда (а еще на x86 и arm у вас все шансы поймать разное поведение). Так что нет смысла все это читать и разбираться, если он случайно узнал, что TreeMap нельзя так использовать, то и в куда более сложных вещах канкаренси, jvm memory model, happens before и т.д. он не понимает ничего. В статье есть что-то про concurrentskiplistmap? - нет, но автор не успокаивается и делает свой ProtectedTreeMap вместо того чтобы просто открыть книгу и почитать…

Да какие "сложные структуры данных"? У вас банальный неатомарный if (flag) { reset_flag_and_do_something(); } поплывёт.

Код на C++ некорректный. Автор кода не понимает основ многопоточности в си++

Так это же намеренно некорректный код показывающий что может случиться при неправильном использовании, на всех языках.

Ну как бы это основы многопоточности, даже не что-то продвинутое.

Увы, я всё равно не понял в чём заключается сравнение с джавой. Какая-то помесь тёплого с мягким.

Есть гораздо более дешёвый способ выкидывать ConcurrentModificationException почти в тех же случаях - просто считать число итераций, и сравнивать с ожидаемой глубиной дерева на основе size. Тоже никаких гарантий, как и с посещенными узлами, но поможет находить ошибку.

А можно использовать потокобезопасные структуры при работе с многопоточностью

Это может сделать пользователь.

А я по то как библиотека может помочь этому пользователю найти проблему и понять что надо синхронизироваться.

А это сработает? Вроде как такие механизмы счётчиков, которые для CME используются сами тоже не защищены от гонки.

Не нужны специальные счетчики. Я предлагаю просто в коде функции которая спускается по дереву в поисках нужного элемента (и входит в бесконечный цикл) завести локальную переменную и считать число итераций.

Т.е. типа раз дерево сбалансированное (а оно такое), то взять лимитом логарифм с хвостиком и при его достижении в глубину сказать "лапки"? Тогда да, хранимых счётчиков избежим.

Да можно лимитом хоть N взять. Уж больше чем O(N) сложность не может оказаться даже в несбалансированном дереве. А производительность в такой ситуации(повреждённая мапа) не основной вопрос.

My name is Joseph, and I love programming, performance analysis, and DotA.

Дота-дрочер рассказывает миру о том, как он открыл thread-safety для себя. Это Хабр в 2025-ом.

Поясните, кто в Java разбирается, как там вообще подобное возможно? Разве виртуальная машина не делает весь код безопасным, оберегая в том числе от таких вот проблем? Если нет, то что тогда происходит? Есть какие-либо гарантии на работу при конкурентном доступе? Или же весь процесс может упасть как в C++?

На С++ может упасть как угодно: access violation, переписать чужую память, вызвать левый код, отформатировать корневой каталог. На Java гарантируется безопасность на уровне виртуальной машины, ничего перечисленного выше быть не может. Но от ошибок на более высоком уровне структур данных это уже никак не защищает. Создать зацикленные ссылки внутри того что должно быть деревом - с точки зрения VM совершенно нормально, она про структуру деревьев ничего не гарантирует. Это имхо в статье самое интересное, автор разобрался как и что именно происходит.

А как в Java гарантируется безопасность? Я что-то не могу представить, как сделать так, чтобы нисинхронизированный доступ чего-то во внутренностях виртуальной машины не ломал. Или же там всё так специально спроектировано, что структуры данных остаются валидными буквально после каждой инструкции?

Любой код, в котором может возникнуть логическая гонка при доступе к данным, должен использовать какой-то механизм для синхронизации параллельных процессов, например, в простейшем случае, мьютексы. Даже процессор внутри себя делает нечто подобное при параллельном исполнении инструкций. Разумеется, это относится и к виртуальной машине.

Меня интересует не как надо правильно, а как авторы виртуальной машины Java умудрились сделать так, что даже при неправильном использовании она не падает. Да, уходит в бесконечный цикл, как у автора статьи, но не падает же!

А почему она должна падать, если свои собственные внутренние структуры данных они обсемафорили? С точки зрения JVM, у автора не происходит ничего неправильного. Он запрограммировал бесконечный цикл, JVM его исполняет.

Реализация класса TreeMap с точки зрения JVM - это пользовательский код. Он исполняется в соответствии со спецификацией байикода.

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

В результате вы можете писать любой бред, но лишь в пользовательские структуры данных, и лишь если с типами всё в порядке. Программа не падает, но исключения бросает, и в пользовательских структурах может быть что угодно.

очень легко возможно, в java честные потоки, а не 1 на весь процесс как в некоторых языках-платформах. Для корректной работы параллельных алгоритмов нужно использовать соответствующие инструменты: классы (локи и т.д.) и конструкции языка (synchronized, volatile), или готовые классы. Процесс упасть не может, просто вы не увидите то значение или событие, на которое рассчитывали в своей программе. В самых первых версиях была идея сделать защиту на случай если код будет выполняться в многопоточной среде, для этого вместо интерфейса Map добавили класс Hashtable, у которого методы синхронизированные, но от этого подхода в итоге отказались, просто потому, что это не бесплатно с точки зрения ресурсов, несмотря на все оптимизации, и нужно далеко не всегда (если Hashtable локальная переменная в методе, то зачем мне синхронизация?), и кстати это все равно не гарантирует что ВЕСЬ ваш код будет корректно работать в многопоточной среде, мало ли вы еще какие ошибки и допущения сделали. Потом были эксперименты и сделали виртуальную машину у которой все переменные были volatile (при обновлении это значение становится видимым сразу всем потокам + записи в другие обычные общие переменных до volatile тоже гарантированно видимы), говорят что она работала раз в 5 медленнее референсной. Так что сейчас виртуальная машина защищает от многих ошибок и проблем, а проблема написания многопоточного кода определена как логическая и проблема разработчика, но есть готовые классы на все случаи жизни и правила по которым работает многопоточный код. В общем нет волшебного способа сделать так, чтобы императивный код начал корректно работать в многопоточной среде без доработок руками и включения головы, все равно вы столкнетесь с какой-либо проблемой

Процесс упасть не может

Какими способами авторы виртуальной машины Java этого добились? Volatile везде, насколько я понял, они таки не используют, значит этот вариант отпадает. Может они Access Violation ловят, если таки случилась проблема, вызванная несинхронизированным доступом?

Семафоры ставят на любые операции записи к атомарным объектам, скорее всего. Поэтому доступ всегда синхронизирован.

Но JVM - хитрая вещь, там могут быть и более сложные техники.

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

из другого вашего комментария, так никакого доступа к внутренним структурам нет, все классы начиная с Object заканчивая TreeMap работают в контексте пользователя под виртуальной машиной, они такой же байт код как и все остальные классы. Конечно есть JIT, но как делается так, что скомпилированный нативный код не крашит процесс я не скажу. Я на си никогда не писал параллельных программ и честно говоря не понимаю почему у вас должен быть крэш, если в переменную будут писать 2 потока, а третий будет ее читать, как по мне это будет просто недетерминированное поведение.

Ну там креш теоретически мог бы быть, если, например, мы уничтожим узел дерева, а указатель из другого узла обновится несинхронно и продолжит ссылаться на уничтоженный узел, то есть на освобождённую память, а дальше в этой освобождённой памяти будет размещён новый объект с другой внутренней структурой, и при доступе по указателям мы пойдём гулять неизвестно куда. Но в JVM такого не может быть, потому что объект для байткода – это и есть ссылка на него, а сборка мусора автоматическая.

Sign up to leave a comment.

Articles