Основная фишка erlang — это все-таки его жуткая распределенность.
Модель программирования основана исключительно на посылке сообщений. То есть никакой расшаренной памяти, никаких,
мьютексов, евентов и прочей ерунды, обычно ассоциирующейся с параллельными (concurrent) приложениями.
Примитивных операций есть три:
- можно создать процесс с помощью функции spawn:
spawn(Function)
- можно ему послать сообщение с помощью выражения !
Pid ! Message
- и получить сообщение
receive Pattern1 -> ... Pattern2 -> ... Pattern3 -> ... after N -> ... end
Конструкция receive очень похожа на выражение case, только для обработки таймаутов можно добавить кейс after N,
где N — время в мс, по истечении которого перестаем ждать ответа.
[Избитый] простейший пример, с процессами, пингующими друг друга:
-module(ping_test). -compile(export_all). ping() -> % создаем процесс pong Pid = spawn(fun pong/0), % посылаем ему сообщение -- атом ping и свой Pid Pid ! {ping, self()}, % ждем от него сообщений ping_loop(Pid). % обработка сообщений от процесса pong ping_loop(Pid) -> receive {pong, Pid} -> % получили сообщение pong и Pid отправителя io:format("received pong~n"), % ждем случайный интервал времени от 0 до 1200 мс. timer:sleep(random:uniform(1200)), % шлем ему опять сообщение ping Pid ! {ping, self()}, ping_loop(Pid) % и опять ждем... after 1000 -> io:format("pong timed out~n") end. % входная точка процесса pong pong() -> receive {ping, Pid} -> % получили сообщение ping и Pid отправителя io:format("received ping~n"), % подождем.. timer:sleep(random:uniform(1200)), % шлем обратно pong Pid ! {pong, self()}, pong() % цикл after 1000 -> io:format("ping timed out~n") end.
Задержки там я вставил чтобы сымитировать как будто процессы действительно как-то серьезно обрабатывают сообщения, причем иногда не укладываются с этим в таймаут в одну секунду. Итак, запускаем:
1> c(ping_test). {ok,ping_test} 2> ping_test:ping(). received ping received pong received ping received pong received ping received pong received ping connection to pong timed out ok 3> connection to ping timed out
Рантайм обеспечивает, что сообщения доставляются в том же порядке, как и были отправлены, и никогда не выпадают из последовательности (то есть, если процесс N+1 сообщение не будет доставлено, если пропало N-ное).
Процессы можно легко и непринужденно запускать и на удаленных машинах, с помощью разновидности той же функции spawn. В большинстве случаев никакой разницы между локальными и удаленными процессами нет.
Чтобы превратить пример с ping в распределенное приложение, нужно просто заменить вызов
spawn(fun ...)
на
spawn(node, fun ...)
Где node это атом-имя узла на котором требуется запустить процесс. виртуальной машины. (А узел (node) — это просто запущенный экземпляр виртуальной машины Erlang).
Итак, немножко изменим функцию ping (модифицированный исходник тут):
ping(Node) -> Pid = spawn(Node, fun pong/0), Pid ! {ping, self()}, ping_loop(Pid).
Теперь запустим два интерпретатора в качестве узлов с именами boo и foo (оба должны иметь доступ к скомпилированному модулю, так что проще запустить их из одной директории):
$ erl -name foo@127.0.0.1
$ erl -name boo@127.0.0.1 (boo@127.0.0.1)2> c(ping_distrib). {ok,ping_distrib} (boo@127.0.0.1)1> ping_distrib:ping('foo@127.0.0.1'). received ping received pong received ping received pong received ping received pong received ping connection to pong timed out ok (boo@127.0.0.1)2> connection to ping timed out
Создание процесса в Эрланге — очень дешевая операция (цифры — порядка сотен тысяч созданных и убитых процессов в секунду, см. бенчмарк здесь ).
Запущенные процессы также жрут очень мало памяти, а в режиме ожидания сообщений — еще и не жрут CPU (например, 500000 таких спящих процессов у меня ест 0% CPU и 600 Мб памяти — около 1.4 кб на процесс).
Механизмы обработки ошибок в Erlang, конечно, тоже распределенные.
В случае ошибки процесс умирает, и система рассылает всем процессам, связанным с ним специальное сообщение exit. Если тот процесс эти сообщения не отлавливает (для чего у процесса должен быть установлен специальный флаг trap_exit), то система убивает и его, и так далее по цепочке.
Связываются процессы с помощью функции link/2. Для удобства есть еще spawn_link/2, и если ее использовать в нашем примере вместо spawn, а также выходить из процессов не нормально, а с помощью erlang:error/1 (см. модифицированный исходник тут), вывод будет немного другим:
4> ping_test:ping(). received ping received pong connection to ping timed out =ERROR REPORT==== 2-Feb-2008::14:30:23 === Error in process <0.43.0> with exit value: {ping_timeout,[{ping_test,pong,0}]} ** exception exit: pong in function ping_test:pong/0
Не рекомендуется использовать другие механизмы, такие как коды ошибок — идеология "let it crash". Скажем, если функция принимает на вход строку — это проблема вызывающего не передавать туда атомы, числа и прочую хрень.
Хотя эти механизмы в Erlang не кажутся на первый взгляд чем-то сверхпродвинутым, именно они помогают писать сложные отказоустойчивые серверные приложения, уделяя при этом минимум времени на написание кода, обрабатывающего ошибки.
13 comments:
Как человек, развращённый Python-ом и Си, заявляю - ну и синтаксис! :)))
Хорошее дело затеял автор. Большое спасибо.
Сам сейчас потихоньку ерланг изучаю и с любопытством прочитал статьи. Жду продолжения :)
По мне так, механизм использующийся в ерланг это прошлый век. Доставка сообщений менее предсказуема и не линейна. Что в свою очередь рожает неограниченное количество ошибок, большее время разработки.
axet> По сравнению с чем?
2axet
она и не должна быть предсказуемой и линейной.
Смысл эрланга в том что система это набор общающихся между собой процессов. Если принять процесс за объект то получаем классическое ООП уровня смоллтолка, но с поддержкой распределённости прям из коробки.
Я правильно понимаю, что программа через некоторое время упадет со stack overflow? ping_loop вызывает сам себя.
Krasin> Нет, как и любой другой функциональный язык, Erlang поддерживает оптимизацию хвостовых вызовов -- т.е. этот рекурсивный вызов ping_loop будет преобразован компилятором в цикл.
О, спасибо. Я забыл про хвостовую рекурсию :)
очень понравилось (вместе с первой частью).
>Хотя эти механизмы в Erlang не кажутся на первый взгляд чем-то сверхпродвинутым, именно они помогают писать сложные отказоустойчивые серверные приложения
Если же добавить, что принципами дизайна заложено автоматическое восстановление упавших процессов(самовосстанавливающееся дерево), то совместно с возможностью замены кода на лету это может убедить каждого, на какие чудеса выносливости способен Эрланг :)
Спасибо за статью, но в реальном приложении почти всегда будет конкурентный ресурс, хранящий данные, доступ к которым для чтения и изменения потребуется нескольким процессам одновременно. Вариант с созданием процесса, являющегося интерфейсом к такому ресурсу, обрабатывающего сообщения других процессов в порядке очереди будет бутылочным горлышком такой системы. Да, сообщения добавляются в очередь и это добавляет устойчивости, но если процесс не успевает обрабатывать сообщения, то они накапливаются, постепенно сжирая память и в итоге он упадет. Как обрабатываются такие моменты в Erlang?
>>Спасибо за статью, но в реальном приложении почти всегда будет конкурентный ресурс, хранящий данные, доступ к которым для чтения и изменения потребуется нескольким процессам одновременно.
И тут мы вплотную подходим к вопросу использования БД... В Erlang есть встроенная БД mnesia для хранения небольших объемов данных, но можно использовать и более мощные РСУБД, если есть такая необходимость.
Erlang - функциональный язык, а то, что вы хотите, называется хранимым состоянием (что в ФП считается дурным тоном). Такими вещами программа заниматься не должна (в большинстве случаев), а должна вместо этого использовать спецсредства. СУБД - одно из таких средств. Прямая передача контекста через параметры функции - второй способ передачи хранимого состояния, но он не всегда применим.
>называется хранимым состоянием
он говорит не о хранимом состоянии.а о том, что будет если процесс не будет успевать разгребать свой ящик сообщений.
Post a Comment