Erlang avalanche
От: Lazy Cjow Rhrr Россия lj://_lcr_
Дата: 12.12.06 15:03
Оценка: 534 (41)
... или почему имплементация Erlang style concurrency под обычные VM очень проблематична (“очень проблематична” – это потому что я ненавижу слово “невозможно”). Я собираюсь довольно сильно углубиться в детали (в конце концов, внимание к деталям – это то что отличает профи от любителя )

1. Хотя даже сами эрланговцы шутят, что soft real time – это “basically nothing”, тем не менее разница между soft real-time и отсутствием вообще каких-либо гарантий существенна. (Для сложных систем обеспечить SRT довольно трудно, и существуют ли вообще сложные системы с HRT я не знаю).

A hard realtime system is one which can guarantee that a certain action will always be carried out in less than a certain time. Many simple embedded systems can make hard realtime guarantees, e.g. it is possible to guarantee that a particular interrupt service routine on a Z80 CPU will never take more than 34us. It gets progressively harder to make such guarantees for more complex systems.
Many telecomms systems have less strict requirements, for instance they might require a statistical guarantee along the lines of "a database lookup takes less than 20ms in 97% of cases". Soft realtime systems, such as Erlang, let you make that sort of guarantee...
Language features which make it hard(er) for programmers to roughly estimate performance were never added to Erlang. For instance, Erlang doesn't have lazy evaluation.
--
Система с жёстким временем это такая, что опеределённое действие будет всегда завершаться в определённый промежуток времени. Множество простых встроенных систем могуть допускать жёсткие временные ограничения, например возможно гарантировать, что прерывание на CPU Z80 никогда не превысит больше 34 микросекунды. И с ростом сложности систем дать подобные гарантии всё тяжелее и тяжелее.
Много телекоммуникационный систем имеют более мягкие ограничения, например, они могут требовать статистическую гарантию скажем "поиск в базе данных требует меньше 20 милисекунд в 97% случаев". Платформы с мягким временем, такие как Эрланг, позволяют вам давать гарантии подобного рода...
В Эрланг никогда не добавлялись фичи, которые бы усложняли оценку времени выполнения. Например, в Эрланге нет ленивых вычислений.
--
(http://www.erlang.org/faq/x1138.html)

Кроме того, для задач телекома важно, чтобы и время отклика было очень мало (речь идёт о милисекундах). Здесь “soft” означает, “мы не можем _абсолютно_ гарантировать всё, но мы проектировали платформу с самого начала так, чтобы можно было _практически_ гарантировать достаточно быстрый отклик”. Это гораздо больше того, что может сказать здесь, скажем, Ява (и это неудивительно, поскольку Ява проектировалась так, чтобы сказать кое-что в другом месте).

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

2. Первый отпечаток – это решение о том, как распределяется время среди процессов. Наприме, в есть аналогичные легковесные процессы в Питоне (stackless microthreads, Candygram), в Яве (Scala actors) и в Лиспе (Distel). В сущности, что такое параллельная программа на лёгких процессах? Грубо говоря, это большой однопоточный while (или хвостово-рекурсивный эквивалент):
while (true)
{
    int n = decide();
    work_in_process(n);
}

Функции, которые пишет программер, при компиляции будут порезаны на мелкие кусочки переходами на внешний уровень, так что поток управления неявно размазывается по функциям work_in_process(n). Очевидно, что пока work_in_process(n) не отработает, передать управление в work_in_process(n+1) мы не можем. Теперь предположим, что программер пишет
    xml_tree = xml.parse();

Внимание вопрос: как выдрать управление из функции parse и передать в work_in_process(n+1)? Если эта функция из фреймворка? Если эта функция из third-party библиотеки? Скала и Питон разводят руками. Distel – потенциально опасные функции помечаются специальным соглашением о наименовании. То же самое придётся делать с функциями, вызывающими эти, функциями второго, третьего и т.п. уровня. Такая операция замыкания на множестве функций вырежет добрую половину (если не больше) всех доступных библиотек и функций.

Файберы не решают этой проблемы, потому что внедрить переход к другому файберу в функцию parse технически трудноосуществимо. Либо похожий (скорее фантастический) вариант – послайсить тяжёлые функции по результатам предварительного прогона – то есть внедрить в их код команды возврата к шедулеру. Однако, во-первых, технически я не представляю как это сделать. Во-вторых, нужно где-то хранить контекст, чтобы потом можно было продолжить выполнение. В-третьих, у этого подхода есть жёсткие ограничения по масштабируемости. В-четвёртых, этот подход плохо подстраивается под меняющиеся условия – если какой-то участок

Остаётся одно – выдрать управление с помощью потоков ОС. Получается, от чего ушли к тому пришли.
По мне так не очень весело, а больше способов я лично не вижу.

Ситуация с Concurrent Haskell и E лучше, но там, насколько мне известно, тоже нельзя дать никаких гарантий даже в предположении идеальной операционки. Единственный конкурент – Oz (как утверждают его авторы). Однако строго говоря там другая модель параллельности ну и опять же – свой рантайм.

Эрланг – это функциональный язык реализованный на основе редукций. В то время как скажем в питоновских микропотоках или скалистых акторах квант времени определяется на основе количества выполненых инструкций байт-кода, в Эрланге квант определяется на основе количества редукций. Разница принципиальная: выполнение одной инструкции байткода может длиться от времени выполнения нескольких машинных инструкций до времени перехода Солнца из стадии жёлтого карлика в стадию красного гиганта. В то же время одна редукция выполняется _всегда_ быстро (верхняя граница существует и весьма мала; кстати поэтому в гвардах можно делать только жёстко ограниченный набор операций).

Это означает что эрланговский процесс (в противовес вышеперечисленным) не может оккупировать процессор на долгое время. Вдобавок, шедулер имеет и обеспечивает необходимую точность: _практически гарантируется_, что процесс получит управление во временном окне X (где X зависит от многих факторов, но для целевых приложений его хватает с запасом). Любой математик/программист может вывести все необходимые вероятностные оценки на время отклика и испытать их практически. Естественно, нужно учитывать вносимые флуктуации железом и операционкой. Если с практической точки зрения, нас волнует доступность сервиса, то следует учесть, что доступность и ограничения на время сильно коррелируют. Возьмите правильное железо, и правильную операционку, и при должном подходе к делу вы тоже получите девять девяток доступности, как в случае мегасвитча AXD-301.

3. “Selective receive” это три маленьких пункта:
1. Если очередь пуста, процесс блокируется.
2. Все сообщения в мэйлбоксе “паттерн-матчатся”, соответствующее сообщение удаляется из мэйлбокса и запускается обработка сообщения, которая может длиться сколь угодно долго.
3. Все сообщения в мэйлбоксе сортируются по категориям так, что наиболее приоритетные сообщения берутся первыми.
“Selective receive” имеет преимущества по сравнению с FIFO:

If one may receive input from multiple, uncoordinated senders, there is no way to predict the order in which the inputs will arrive. So you either explicitly handle all permutations, or find a way to reorder messages where applicable. This is essentially what the selective receive does...
This radically reduces the number of combinations we are forced to write code for.
--
Если процесс получает на вход сообщения от нескольких хаотических отправителей, невозможно предсказать порядок получения сообщений. Поэтому вы либо явно обрабатываете все перестановки, или находите способ перестроить сообщения для обработки. Это в сущности то, что делает selective receive...
Это значительно уменьшает количество комбинаций, для которых мы должны написать код.
--
(Ulf Wiger, http://www.erlang.org/ml-archive/erlang-questions/200606/msg00213.html)

Однако требуются значительные усилия, чтобы реализовать его эффективно. А именно, (1) как только в мэйлбокс некоторого приоритетного процесса приходит сообщения, он (процесс) должен мгновенно получить управление (прямой доступ к шедулеру), (2) чтобы разбить сообщения на категории, нужно в худшем случае сканировать весь мэйлбокс, следовательно сканирование может занять большое время, (3) в силу специфики приложений receive запросто может стать узким местом, (4) реализация receive требует некоторой возни с массивами, в частности in-place resize, а для этого нужно взаимодействие с gc.
Считаю, что этих четырёх пунктов должно быть достаточно для того, чтобы реализовать receive на уровне виртуальной машины, а не на уровне байткода.
(Подробнее про семантику selective receive читаем мегасообщение от Richard’а O’Keefe)

4. GC в Эрланге – это произведение искусства.  Ну ладно, просто классный:

Armstrong and Virding have proposed a one-pass mark-and-sweep garbage collector. The collector is designed for languages that prohibit destructive operations (i.e. values can not be overwritten), and is used in a Erlang implementation. Since destructive operations are not allowed, references can only refer to older objects. The collector marks the objects from the youngest to the oldest. If no other object has referred to an object that is being marked, it can be reclaimed immediately (since only younger objects can refer to it.) All objects are kept in a singly linked list to keep the objects in order. All GC operations have predictable runtime behavior. Thus, it can be used for real-time systems if it is scheduled to guarantee memory availability.
--
Армстронг и Вирдинг предложили однопроходной mark-and-sweep сборщик мусора. Сборщик спроектирован для языков, которые запрещают деструктивные операции (то есть значения не могут быть перезаписаны), и используется в реализации Эрланга. Поскольку деструктивные операции недопустимы, ссылки могут лишь указывать на более старые объекты. Сборщик помечает объекты начиная с самых новых и заканчивая самыми старыми. Если нет других объектов, ссылающихся на помеченный, этот объект может быть немедленно освобождён (поскольку только более новые объекты могуть на него ссылаться). Все объекты завязаны в список и образуют порядок. Все операци сборщика имеют предсказуемое время работы. Таким образом, такой сборщик может быть использован для систем реального времени чтобы гарантировать выделение памяти, если есть гарантия того, что сборщик получит управление.
--
([AV95] Joe Armstrong and Robert Virding. One-pass real-time generational mark-sweep garbage collection.)

То есть мало того, что он быстрый (один проход по списку – вот и вся сборка мусора), так ещё имеет предсказуемое время работы.
По этой теме можно также взглянуть сюда (ссыла от Cyberax )
Это ещё не всё. Чтобы увеличить независимость процессов, каждый процесс обслуживается сборщиком отдельно, то есть имеет собственную кучу. Преимущество – можно просто выбросить все данные для процесса, когда этот процесс мрёт. Расшаривание одной большой кучи может привести к более частым сборкам мусора, что нежелательно.
GC должен одинаково хорошо работать как в случае частых перезапусков процессов, так и в случае множества долгоживущих многососущих и многовыплёвывающих процессов.
В то же время, можно сделать так, что если какому-то критическому процессу не будет хватать памяти, то ему будет выделена память за счёт бездействующих процессов.
Наконец, в последнее время появилась “гибридная” модель с расшаренной кучей, где локальная посылка сообщений проходит вообще без какого-либо копирования (см. erl -hybrid) и с сохранением SRT. Это вообще принципиально э... очень проблематично без взаимодействия с gc.

5. Я конечно ляпнул про “интеллектуальность” шедулера. На самом деле он просто удовлетворяет ряду условий:

- scheduling is preemptive.
— a process is suspended if it enters a receive statement, and there is no matching message in the message queue.
— if a receive timer expires, the process is rescheduled.
---
— многозадачность вытесняющая.
— процесс останавливается всякий раз, когда он доходит до receive, и в очереди нет подходящих под паттерны сообщений.
— если время таймера (условие after в операторе receive) истекает, процесс будет перепланирован для того, чтобы первым получить time-slice.
---
(Ulf Wiger, http://www.erlang.org/ml-archive/erlang-questions/200104/msg00072.html)

Обратите внимание на первый и третий пункты. Чтобы обеспечить эти пункты, нужно иметь возможность шедулеру в любой момент получить управление (вот он SRT). И для совсем тяжёлых случаев у нас есть

In a runtime environment that supports it, it should be possible to also have erlang processes at interrupt priority (meaning that they will be allowed to run as soon as there is something for them to do — not having to wait until a 'normal' priority process finishes its timeslice.)
--
В окружении, поддерживающем прерывания, также заложена возможность иметь процессы с приоритетом "interrupt" (что означает, что они должны получить управление как только будет что-то им для выполнения — то есть нет необходимости ждать, пока процесс с нормальным приоритетом доработает до конца своего time-slice'а).
--
(Ulf Wiger, http://www.erlang.org/ml-archive/erlang-questions/200104/msg00072.html , конец сообщения)

То есть здесь конечно рулит не сам шедулер как таковой, а то что он тесно склеен с рантаймом и поэтому обеспечивает такие возможности.

Ну я надеюсь, я достаточно подробно пояснил, почему я считаю, что Эрланг невозможен под jvm/.net? Хочу правильно поставить акцент: я не отрицаю возможность lightweight библиотеки, я всего лишь навсего очень-очень сильно сомневаюсь, что она может составить конкуренцию Эрлангу на _его_ поле боя и на смежных площадях:

Telephone systems have to handle 95% of maximum supported load while being attacked with 100 times that load.
Erlang is designed for telephone systems by the biggest supplier of telephone systems in the world. It works.
--
Системы телефонии должны работать при нагрузке 95% от максимально возможной при этом выдерживать пиковую нагрузку превышающую максимальную в 100 раз. Эрланг был создан для систем телефонии самым большим поставщиком таких систем в мире. И он работает.

quicksort =: (($:@(<#[),(=#[),$:@(>#[)) ({~ ?@#)) ^: (1<#)
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.