В тази глава ще разгледаме процесите. Това са интегралните градивни елементи, които позволяват на Elixir да премине от "Oh cool, it's like a functional version of Ruby" към "Oh my god, it's literal witchcraft".

В добрия смисъл, очевидно.

Това също трябва да помогне за повишаване на осведомеността за някои от подробностите за изпълнението на Elixir и Erlang. И така, нека започнем с дизайнерските цели на самия Erlang.

Джо Армстронг, съизобретател на Erlang, заяви, че възгледът за езика трябва да се основава на следните цели:

Everything is a process.
Processes are strongly isolated.
Process creation and destruction is a lightweight operation.
Message passing is the only way for processes to interact.
Processes have unique names.
If you know the name of a process you can send it a message.
Processes share no resources.
Error handling is non-local.
Processes do what they are supposed to do, or fail.

Тъй като Elixir е изграден върху Erlang, ние можем да се възползваме напълно от тези принципи на проектиране безплатно.

Процесите и концепцията за предаване на съобщения не са нови и не са уникална функция за Elixir/Erlang. Корените на този дизайн могат да бъдат намерени в цялата компютърна наука, най-вече в модела на актьора и основните аспекти на ООП. Успехът на дизайна на Erlang доведе до подобни реализации на други езици, например Akka в Java/Scala и Celluloid в Ruby.

Фокусирайки се върху Elixir, всичко работи вътре в процес. Можем да демонстрираме това, като отворим празна iex сесия и направим следното:

iex> :observer.start

Тук, дори с празна iex сесия, можем да видим, че има множество процеси, които се изпълняват, за да направят REPL достъпен за нас.

Тези процеси са невероятно евтини по отношение на процесора и паметта, така че създаването на хиляди процеси не е голяма работа. Можем да видим това, като държим прозореца на наблюдателя отворен и използваме следния пример от Programming Elixir:

Ако наблюдаваме диаграмите на натоварването, когато изпълняваме примера, ще видим нещо подобно:

Така че, дори когато създаваме милион процеси, ние използваме по-малко от 50% от нашия планировчик — и използването на паметта има прекрасен симетричен пик и дъно. Много добре.

Ако не сте отворили наблюдателя, единственото нещо, което бихте забелязали, са c5 секунди, необходими за изпълнение на заявката.

Нека да преминем към някои по-прости примери, за да разберем какво се случва и как да ги тестваме!

Тук имаме много прост процесен модул. start/0 ще spawn процес, който ще изпълни функцията, дадена като атом (в този случай :receiver) от предоставения модул (в този случай __MODULE__, който е съкратена препратка за Chapter4.BasicProcess ) и ще предаде предоставените аргументи — тук празен списък.

Функцията receiver/0 декларира типовете съобщения, които може да обработва. Това следва основния дизайн на предаването на съобщения:

If the object responds to the message, it has a method for that message.

Ако си спомним дизайна на Erlang, този нов процес има една работа:

Processes do what they are supposed to do, or fail.

Феновете на Рик и Морти могат да мислят за процесите от гледна точка на г-н Мийсийкс, с изключение на това, че времето, необходимо за извършване на работата им, не води до болка... Това означава, че е възможно да го помолите да премахне два удара от играта ви!

Изпълнението на този код ще ни даде следното:

Този процес, галено известен като #PID<0.126.0>, просто ще стои там и ще чака търпеливо, докато получи съобщение. Можем да проверим дали чака там, като попитаме дали е жив:

Можем също така да намерим повече информация за нашия процес, така че нека го направим:

Това е вътрешното представяне на процес. Важните части за тази статия са, че тя в момента чака и че пощенската кутия на процеса, messages: []на снимката, е празна.

И така, докато чака, нека му изпратим съобщение:

Да, то отговори.

Сега нека проверим здравето му:

Аннн, няма го :(

Нека да разгледаме тестването на този прост сценарий за създаване на процес и изпращане на съобщение до него:

В първия тест всъщност трансформираме входа от Process.info/1 в карта line 9. Това ни позволява да съвпадаме по шаблона на важните части. Това е полезно, тъй като PIDs са динамични и така че трябва да кодирате всичко, за да съответства на това, което съществува в списък, просто няма да се случи.

Във втория тест сме подли и се възползваме от :erlang.trace. Това ще ни позволи да достигнем до основния Erlang и да следим съобщенията, които нашият създаден процес получава.

След това изпращаме съобщение до нашия процес и използваме assert_receive на ExUnit, за да проверим дали нашият процес наистина е получил съобщението във формата, в който сме го изпратили.

Можете да видите, че тук има много малки движещи се части и се надяваме, че имате усещането, че това вероятно е на ниво по-ниско от това, което всъщност ни интересува за нашето приложение...

В момента нашият процес умира след една работа. Това не е идеално, така че трябва да се справим с това, като придадем на нашия процес малко „постоянство“.

Преди това обаче има малък, но интересен ръбов случай, който трябва да разгледаме:

Когато създавате/унищожавате процес, всъщност има малко режийни разходи, свързани с него. Това означава, че ако сме достатъчно бързи, можем да изпратим на процес няколко съобщения и той ще ги получи. Въпреки това, ако си спомняте, един процес има само една работа. Това означава, че изпращането на няколко съобщения ще доведе до това, че всички те ще останат в пощенската кутия на процеса — обаче само първото получено ще бъде обработено. Останалите просто ще останат в пощенската кутия на процеса, докато не бъдат събрани за боклук.

Със сигурност не очаквах това и реших, че е доста изпипано. Безполезен, но доста чист. Можете да го добавите към знанията си за пъб викторина за следващия път, когато има кръг за странностите на процеса на Elixir.

Възможно е да трансформираме нашия процес от способността да обработва само едно съобщение до обработка на множество съобщения. Необходим е само един ред код:

Това означава, че след като обработи съобщение, функцията ще се извика отново и ще изчака друго съобщение.

Начинът, по който работи това, е наистина фин, но невероятно готин и достоен за познато пляскане за голф. И така, ето го!

Когато създадете процес, той създава уникален PID. Когато процес умре и създадете друг, ще ви бъде присвоен друг уникален PID:

Това не е особено полезно, така че чрез повторно извикване на функцията като последната стъпка от извикване на функция, ние извършваме рекурсивно извикване. Обикновено това би означавало добавяне на нова стекова рамка към стека на повикванията. Elixir / Erlang обаче са част от „група езици“, които се възползват от Tail-Call Optimization .

Това означава, че функцията се оценява като „самоизвикваща се“. Вместо да разпределя нов PID или да създава нов екземпляр на функцията, той се държи подобно на команда GOTO и просто скача отново до върха на функцията. Това по същество действа като най-близкия Elixir, който някога ще стигне до цикъл.

Имайки това предвид, нека видим това на практика - първо в командния ред, след това в тестове:

Обърнете внимание, че през цялото време сме запазили същия PID.

Това е подобно на стъпките, предприети в командния ред.

Странична бележка за изпращане на множество съобщения:

Подобно на режийните разходи в BasicTest има леки режийни разходи при получаване на съобщения. line 36 съдържа сън от 1 ms, това е необходимо, за да се гарантира съгласуваност с преминаването на тестовете.

Не е страхотно.

Досега нашите съобщения бяха еднопосочни - не е необходимо да бъдат. Можем да изпращаме съобщения обратно на всеки/който ни е изпратил, обикновено като по-разумен начин да потвърдим, че съобщението е стигнало до местоназначението си.

Това се прави само с леко ощипване на нашия модул RecursiveProcess:

Тук добавяме още един аргумент към нашия кортеж от съобщения, наречен „повикващ“. Това е мястото, където ще предадем PID на процеса, който изпраща съобщението, за да знае да отговори. Можем да го видим на практика в командния ред:

Тук можем да видим, че предаваме PID на терминала iex като автор на съобщението и можем да видим, че получаваме отговор обратно от ResponderProcess. Трябва да използваме flush/0, за да изпразним пощенската кутия на процеса — това може да бъде полезно, когато няма изричен начин за обработка на получените съобщения, но отново… не е страхотно нещо за голямо приложение.

Начин за тестване на този поток е:

Много подобно на примера с командния ред, но не е необходимо да извикваме flush/0.

Има много повече работа с процесите и различните им варианти, които съществуват. На този етап трябва да имаме основно разбиране за следното:

  • Всичко в Elixir е процес
  • Можем свободно да създаваме процеси
  • Процесите имат една работа
  • Процесите на хвърляне на хайвера са евтини
  • Можем да предаваме съобщения от процес на процес само като знаем техния PID
  • Как да тествате основното предаване на съобщения

Също така е ключово да имаме това разбиране, тъй като забавните неща, базирани на OTP, които получаваме от Elixir, са изградени върху тези концепции. GenServers. например са безумно мощен инструмент, който се основава на предаване на съобщения около приложение.

В цялата тази глава бихте забелязали, че има няколко неудобни момента в опитите да ги тествате, като множество съобщения и използване на трикове за време. Всъщност, ако изпълнявате тестовия пакет локално, има моменти, в които тестовете ще се провалят единствено поради времето / режийните разходи на процесите.

Тук има и по-голямо внимание; граничим с тестването на реалната реализация на самия език. Това рядко е добра идея. Имаме и други начини да тестваме дали съобщенията се предават успешно, например - направихме трите предишни глави, демонстрирайки това, и ще продължим да го правим в други глави.

Хосе спомена това в тази „публикация във форума“, случайно оттук получих и идеята за проследяване.

Надявам се тази глава да ви хареса. Ако има въпроси или части, на които трябва да се обърне повече внимание, оставете отговор, в противен случай ще преминем към следващата тема.