Процессы Эрланга против потоков Java

Я читаю книгу "Эликсир в действии" Саша Джурича, а в первой главе говорится:

Процессы Эрланга полностью изолированы друг от друга. Они делят нет памяти, и сбой одного процесса не вызывает сбой других процессы.

Не так ли верно и для потоков Java? Я имею в виду, когда поток Java падает, он тоже не разбивает другие потоки - особенно, если мы смотрим на потоки обработки запроса (позволяет исключить поток main из этой дискусции)

Ответ 1

Повторяйте за мной: "Это разные парадигмы"

Скажи это вслух 20 раз или около того - это наша мантра на данный момент.

Если мы действительно должны сравнить яблоки и апельсины, давайте хотя бы рассмотрим, где пересекаются общие аспекты "быть фруктами".

Java-объекты являются базовой единицей вычислений для программиста Java. То есть объект (в основном структура с руками и ногами, инкапсуляция которой более строго соблюдается, чем в C++) является основным инструментом, с помощью которого вы моделируете мир. Вы думаете: "Этот объект знает/имеет Data {X,Y,Z} и выполняет над ним Functions {A(),B(),C()}, переносит Data куда угодно и может связываться с другими объектами, вызывая функции/методы, определенные как часть их открытого интерфейса. Это существительное, и это существительное делает вещи. " То есть вы ориентируете свой мыслительный процесс на эти вычислительные единицы. Случай по умолчанию состоит в том, что вещи, которые происходят среди объектов, происходят последовательно, и сбой прерывает эту последовательность. Они называются "объектами" и, следовательно, (если мы не примем во внимание изначальное значение Алана Кея), мы получим "объектную ориентацию".

Erlang-процессы - это базовая единица вычислений для программиста Erlang. Процесс (в основном автономная последовательная программа, работающая в своем собственном времени и пространстве) является основным инструментом, с помощью которого Эрлангер моделирует мир (1). Подобно тому, как объекты Java определяют уровень инкапсуляции, процессы Erlang также определяют уровень инкапсуляции, но в случае Erlang единицы вычислений полностью отрезаны друг от друга. Вы не можете вызывать метод или функцию в другом процессе, а также не можете получить доступ к каким-либо данным, которые в нем находятся, и ни один процесс не выполняется даже в том же контексте синхронизации, что и любые другие процессы, и нет никакой гарантии относительно порядка относительного приема сообщений. другим процессам, которые могут отправлять сообщения. С таким же успехом они могут быть на разных планетах (и, если подумать, это действительно правдоподобно). Они могут аварийно завершить работу независимо друг от друга, и другие процессы будут затронуты только в том случае, если они преднамеренно решили быть затронутыми (и даже это включает обмен сообщениями: по сути, регистрация для получения сообщения о самоубийстве от мертвого процесса, который сам по себе не гарантированно поступит в каком-либо виде). порядка относительно системы в целом, на которую вы можете или не можете реагировать).

Java имеет дело со сложностью непосредственно в составных алгоритмах: как объекты работают вместе, чтобы решить проблему. Он предназначен для этого в пределах одного контекста выполнения, и по умолчанию в Java используется последовательное выполнение. Множество потоков в Java указывает на несколько работающих контекстов и является очень сложной темой из-за воздействия действий в различных контекстах синхронизации друг на друга (и системы в целом: отсюда и защитное программирование, схемы исключений и т.д.). Выражение "многопоточный" в Java означает нечто иное, чем в Erlang, фактически, в Erlang об этом даже не говорят, потому что это всегда базовый вариант. Обратите внимание, что потоки Java подразумевают сегрегацию в отношении времени, а не памяти или видимых ссылок - видимость в Java контролируется вручную путем выбора того, что является частным, а что - открытым; общедоступные элементы системы должны быть либо спроектированы так, чтобы быть "ориентированными на многопотоковое исполнение" и повторно входящими, секвенироваться через механизмы очередей, либо использовать механизмы блокировки. Вкратце: планирование - это проблема, управляемая вручную в многопоточных/параллельных программах Java.

Erlang разделяет контекст выполнения каждого процесса с точки зрения времени выполнения (планирование), доступа к памяти и видимости ссылок, и при этом упрощает каждый компонент алгоритма, полностью изолируя его. Это не просто случай по умолчанию, это единственный случай, доступный в рамках этой модели вычислений. Это происходит за счет отсутствия точного знания последовательности какой-либо операции после того, как часть ваших последовательностей обработки пересекает барьер сообщений - потому что все сообщения по существу являются сетевыми протоколами, и нет вызовов методов, которые могут быть гарантированно выполнены в пределах заданного контекст. Это было бы аналогично созданию экземпляра JVM для каждого объекта и разрешению им обмениваться данными только через сокеты - это было бы смехотворно громоздко в Java, но способ, которым Erlang предназначен для работы (между прочим, это также основа концепции написания "Java microservices", если кто-то бросает веб-ориентированный багаж, к которому часто прибегают модные слова - программы Erlang по умолчанию являются роями микросервисов). Это все о компромиссах.

Это разные парадигмы. Самая близкая общность, которую мы можем найти, состоит в том, чтобы сказать, что с точки зрения программиста процессы Erlang аналогичны объектам Java. Если мы должны найти что-то для сравнения потоков Java с... ну, мы просто не собираемся найти что-то подобное в Erlang, потому что в Erlang нет такого сопоставимого понятия. Бить мертвого коня: это разные парадигмы. Если вы напишете несколько нетривиальных программ на Erlang, это станет очевидным.

Обратите внимание, что я говорю "это разные парадигмы", но даже не затронул тему ООП против ФП. Разница между "мышлением в Java" и "мышлением в Erlang" более фундаментальна, чем ООП против FP.

Хотя верно то, что Эрланг, "ориентированный на параллелизм" или "ориентированный на процесс", ближе к тому, что имел в виду Алан Кей, когда он придумал термин "объектно-ориентированный" (2), на самом деле это не имеет значения. Кей получал то, что можно уменьшить когнитивную сложность системы, разрезая ваши компьютеры на отдельные куски, и для этого необходима изоляция. Java выполняет это способом, который делает его по-прежнему принципиально процедурным по своей природе, но структурирует код вокруг специального синтаксиса над диспетчерскими замыканиями более высокого порядка, называемыми "определениями классов". Эрланг делает это, разделяя текущий контекст на объект. Это означает, что штуковины Эрланга не могут вызывать методы друг у друга, но штуковины Java могут. Это означает, что вещи Erlang могут аварийно завершиться, но вещи Java не могут. Из этого основного различия вытекает огромное количество последствий - отсюда и "разные парадигмы". Компромисс.


Примечания:

  1. Кстати, Erlang реализует версию " модели актера ", но мы не используем эту терминологию, так как Erlang предшествует популяризации этой модели. Джо не знал об этом, когда он разработал Erlang и написал свою диссертацию.
  2. Алан Кей довольно много рассказал о том, что он имел в виду, когда придумал термин "объектно-ориентированный", наиболее интересным было его восприятие обмена сообщениями (одностороннее уведомление от одного независимого процесса с его собственным временем и памятью другому), VS вызывает ( вызовы функций или методов в контексте последовательного выполнения с совместно используемой памятью) и то, как немного размываются линии между интерфейсом программирования, представленным языком программирования и реализацией ниже.

Ответ 2

Скорее всего, нет. Все потоки в Java имеют одно и то же адресное пространство, поэтому один поток может уничтожить вещи, принадлежащие другому потоку. В Erlang VM это просто невозможно, поскольку каждый процесс изолирован от всех остальных. Это все. Каждый раз, когда вы хотите, чтобы один процесс делал что-то с данными другого, ваш код должен отправить сообщение другому процессу. Единственными вещами, разделяемыми между процессами, являются большие двоичные объекты, которые неизменны.

Ответ 3

Процессы Java фактически могут совместно использовать память. Например, вы можете передать один и тот же экземпляр до двух отдельных потоков, и оба могут управлять своим состоянием, что приведет к потенциальным проблемам, таким как deadlocks.

Elixir/Erlang, с другой стороны, рассматривает это по понятию неизменности, поэтому, когда вы передаете что-то процессу, это будет копия исходного значения.

Ответ 4

когда поток Java умирает, он также не влияет на другие потоки

Позвольте мне спросить о противодействии: почему, по вашему мнению, Thread.stop() устарели более десяти лет? Причина в том, что это именно отрицание вашего заявления выше.

Чтобы дать два конкретных примера: вы stop() поток, в то время как он выполняет что-то как безобидное звучание как System.out.println() или Math.random(). Результат: эти две функции теперь разбиты на всю JVM. То же самое относится к любому другому синхронизированному коду, которое может выполнить ваше приложение.

если мы смотрим на потоки обработки запросов

Приложение теоретически может быть закодировано таким образом, чтобы никогда не использовался абсолютно общий ресурс, защищенный блокировками; однако это только поможет указать точную степень, в которой Java-потоки являются взаимозависимыми. И достигнутая "независимость" будет относиться только к потокам обработки запросов, а не ко всем потокам в таком приложении.

Ответ 5

Чтобы дополнить предыдущие ответы, потоки Java имеют два типа: daemon и non daemon.

Чтобы изменить тип потока, вы можете вызвать .setDaemon(boolean on). Разница в том, что поток демона не позволяет JVM отказаться. Как пишет Javadoc for Thread:

Виртуальная машина Java завершает работу, когда все потоки выполняются только потоками демона.

Это означает: пользовательские потоки (те, которые специально не установлены для демона) не позволяют JVM завершить работу. С другой стороны, потоки демона МОГУТ БЫТЬ ЗАВЕРШЕНО, когда все не-демонные потоки закончены, и в этом случае JVM завершит работу. Итак, чтобы ответить на ваш вопрос: вы можете запустить поток, который не покидает JVM, когда он закончит.

Что касается сравнения с Erlang/Elixir, не забывайте: это разные парадигмы, как уже упоминалось.

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

Ответ 6

Не так ли верно и для потоков Java? Я имею в виду, когда поток Java падает, он тоже не разбивает другие потоки.

Да и нет. Я объясняю:

  • Обращение к разделяемой памяти: разные потоки в процессе Java разделяют всю кучу, поэтому потоки могут взаимодействовать с огромным количеством запланированных и незапланированных способов. Однако объекты в стеке (например, контекст, переданный вызываемому методу) или ThreadLocal являются их собственным потоком (если только они не начинают разделять ссылки).

  • Сбой: если поток сбой в Java (a Throwable распространяется на Thread.run(), или что-то зацикливается или блокируется), эта ошибка может не влиять на другие потоки (например, пул соединений на сервере будет продолжать функционировать). Однако, поскольку взаимодействуют разные потоки. Другие потоки будут легко перемещаться, если один из них заканчивается ненормально (например, один поток пытается прочитать из пустого канала из другого потока, который не закрыл его конец). Поэтому, если разработчики не очень осторожны с paranoid, очень вероятно, что будут возникать побочные эффекты.

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