| theme | uncover |
|---|---|
| style | .small-text { font-size: 0.75rem; letter-spacing: 1px; font-family: "Times New Roman", Tahoma, Verdana, sans-serif; } section { font-size: 28px; letter-spacing: 1px !important; } li { font-size: 28px; letter-spacing: 1px !important; } p.quote { line-height: 38px; } q { font-size: 32px; letter-spacing: 1px !important; } cite { text-align: right; font-size: 28px; margin-top: 12px; margin-bottom: 128px; } code.language-elixir { font-size: 80%; background: #f2f0ed; color: #080808; } li code, h4 code, p code { color: #089808; background-image: linear-gradient(to bottom, #E0EAFC, #CFDEF3); border-radius: 15px; } section { letter-spacing: 1px !important; } span.hljs-comment, span.hljs-quote, span.hljs-meta { color: #3d3f8f; } .hljs-keyword, .hljs-selector-tag, .hljs-tag, .hljs-name { color: #096a6a; } .hljs-attribute, .hljs-selector-id { color: #ffffb6; } .hljs-string, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-addition { color: #489707;; } .hljs-subst { color: #526228; } .hljs-regexp, .hljs-link { color: #e9c062; } .hljs-title, .hljs-section, .hljs-type, .hljs-doctag { color: #724b99; } .hljs-symbol, .hljs-bullet, .hljs-variable, .hljs-template-variable, .hljs-literal { color: #b90f0f; } .hljs-number, .hljs-deletion { color:#ff73fd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } |
| paginate | true |
| footer | Курс по Elixir 2023, ФМИ |
| backgroundImage | linear-gradient(to bottom, #E0EAFC, #CFDEF3) |
| marp | true |
- Какво са процесите?
- Как комуникират процесите?
- Какво е OTP?
Досега говорихме, че процесите могат да си комуникират САМО чрез размяна на съобщения...
- С Erlang/OTP идва и Erlang Term Storage или ETS.
- ETS представлява ключ-стойност база данни, живееща в паметта, която е част от BEAM виртуалната машина.
- Тя НЕ Е имплементирана на Erlang. Вместо това е вградена в самата виртуална машина.
- Това означава, че е написана на C и е оптимизирана за конкурентно писане и четене, а вътрешно съхранява данни, които са MUTABLE.
- Понякога ни се налага да имаме процес, който съхранява състояние, което трябва да е достъпно от няколко процеса.
- Пример : имаме някакъв service (HTTP Server?) и искаме да си водим статистика, кой потребител, колко request-а е направил.
- Бихме могли да използваме тази информация за доста неща...
- Как бихме могли да направим това?
defmodule RequestsPerUser do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
def init(_), do: {:ok, %{}}
def update(user) do
GenServer.cast(__MODULE__, {:update, user})
end
def get(user), do: GenServer.call(__MODULE__, {:get, user})
def handle_call({:get, user}, _, state) do
{:reply, Map.get(state, user, 0), state}
end
def handle_cast({:update, user}, state) do
{:noreply, Map.update(state, user, 1, & &1 + 1)}
end
end- За всеки request към нашата услуга, даден процес ще прави нещо и ще връща резултат на потребителя.
- Преди да върне този резултат, ще извиква RequestsPerUser.update/1 за да увеличи броя на request-ите за дадения потребител:
RequestsPerUser.update("pe6o")
#=> :ok- При 1000 рекуеста на секунда, това означава, че на процеса RequestsPerUser ще бъдат изпращани 1000+ съобщения за всяка секунда.
- Всяко от тези съобщения се обработва последователно.
- Извикване до RequestsPerUser.get/1 може да отнеме дадено време...
- Имаме bottleneck: много процеси използват един процес за пазене и промяна на споделено състояние.
- Ако една програма използва множество нишки/процеси за да постигне конкурентно писане и четене, ако има една нишка/процес, до която всички други опират, все едно имаме последователна програма.
- Как да имплементираме споделено състояние между процеси с конкурентен достъп?
- Отговорът е ETS.
- Както споменахме ETS означава Erlang Term Storage.
- Всеки процес в Elixir/Erlang си има собствен стек и heap
- Също така има споделена памет за по-големите binary-та.
- Има и още една споделена памет - ETS.
- ETS не е имплементирана на Erlang и не е изградена от Erlang процеси и persistent структури като Map или List.
- ETS е част от виртуалната машина, написана е на C и структурите, които използва са mutable.
- Интерфейсът към ETS е на Erlang и отвън може да се приеме, че до таблиците ѝ имаме достъп чрез съобщения, но това не е така.
- Достъпът до таблица в ETS е доста бърз и конкурентен.
- ETS се състои от множество таблици.
- Всяка таблица може да се създаде от който и да е процес и се притежава от процес.
- Има 4 типа таблици:
:set,:ordered_set,:bagи:duplicate_bag. - Всяка таблица има множество записи, които винаги са представени от tuple.
- Един елемент от всеки от тези tuple-и e ключ и по него може да се търси с бърз достъп.
- Тези таблици могат да имат запис с даден ключ само веднъж.
- Имплементирани са с линейна хеш таблица, която обещава бърз достъп за четене и писане.
- Тази хеш таблица е mutable.
- Това е типа на таблиците в ETS по подразбиране.
- Тези таблици се държат по същия начин като
:setтаблиците, но записите им са сортирани по ключовете си. - Възможно е да обхождаме тези записи, използвайки наредбата им.
- Имплементирани са чрез AVL дърво и имат достъп за четене и писане от порядъка на O(log(n)).
- Таблиците от тип
:bagпозволяват множество записи с един и същи ключ, които обаче не може да са напълно еднакви. - Пример:
{1, "пе6о", 16}
{1, "то6о", 16}- При
:bagняма как да имаме:
{1, "пе6о", 16}
{1, "пе6о", 16}- Ако искаме да имаме напълно повтарящи се записи, ще използваме
:duplicate_bag. - Тези типове таблици са имплементирани чрез същия тип хеш таблица като
:setтаблиците.
- Когато се създаде ETS таблица, тя принадлежи на процес.
- Когато този процес спре да съществува, таблицата или се предава на друг процес, или се изчиства от паметта.
- Има три възможни типа достъп до ETS таблици.
- Процесът, който притежава таблица може да чете и пише в нея.
- Останалите процеси могат само да четат от дадената таблица.
- Това е достъпът по подразбиране.
- Процесът, който притежава таблица може да чете и пише от нея.
- Останалите процеси нямат достъп до таблицата.
- Всички процеси имат както достъп за четене, така и достъп за писане до таблицата.
- Таблици могат да бъдат създавани от който и да е процес.
- Този процес става собственик на таблицата.
- За работа с ETS таблици, използваме Erlang-ския модул
:ets.
:ets.new(:users, [])
#=> #Reference<0.4003328283.155844609.148120>table = :ets.new(:users, [])
#=> #Reference<0.4003328283.155844609.148120>:ets.info(table)
#=> [
#=> read_concurrency: false,
#=> write_concurrency: false,
#=> compressed: false,
#=> memory: 300,
#=> owner: #PID<0.89.0>,
#=> heir: :none,
#=> name: :users,
#=> size: 0,
#=> node: :nonode@nohost,
#=> named_table: false,
#=> type: :set,
#=> keypos: 1,
#=> protection: :protected:ets.insert(
:users,
{"meddle", 38, "Sofia", [:elixir, :gaming, :meddling]}
)
#=> ** (ArgumentError) argument error:ets.insert(
table,
{"meddle", 38, "Sofia", [:elixir, :gaming, :meddling]}
)
#=> true
:ets.info(table, :size)
#=> 1:ets.new(:users, [:named_table]) # Сега ще е атом
#=> :users
:ets.info(:users, :named_table)
#=> true
:ets.insert(
:users,
{"meddle", 38, "Sofia", [:elixir, :gaming, :meddling]}
)
#=> true- Нека имаме таблица, която съдържа набор от хора. В нея ще имаме:
- Искаме редовете да са сортирани по nick-овете и таблицата да е достъпна за четене и писане отвсякъде:
:ets.new(
:people, [:named_table, :public, :ordered_set, keypos: 3]
)
#=> :people- insert - Добавя редове, презаписва съществуващи:
:ets.insert(
:people,
[
{
"Петър", "Петров", "pe60", 55,
["Еврофутбол", "мачове", "ракийка и салатка", "Цеца"]
},
{
"Милена", "Стоева", "milen4it0", 23,
["социални мрежи", "кафенца", "клубчета", "дрешки"]
}
]
)
#=> true- insert - Добавя редове, презаписва съществуващи:
:ets.info(:people, :size)
#=> 2- insert_new - Не създава ако нещо съществува вече:
:ets.insert_new(
:people,
[
{
"Милена", "Стоева", "milen4it0", 23,
["социални мрежи", "кафенца", "клубчета", "дрешки"]
},
{
"Слави", "Боянов", "reductions", 25,
["база данни", "elixir", "бридж", "разсъждения!"]
}
]
)
#=> false- insert_new - Не създава ако нещо съществува вече:
:ets.info(:people, :size)
#=> 2- Можем да добавим и по-големи или по-малки tuple-и.
- Важното е да имат поне толкова елементи, колкото е
:keypos. - В нашия случай - три. Ако опитаме да добавим кортеж с два елемента:
:ets.insert(:people, {"Слави", "Боянов"})
#=> ** (ArgumentError) argument error
#=> (stdlib) :ets.insert(:people, {"Слави", "Боянов"})- Има много начини да четем от ETS таблици.
- Най-простият начин е да търсим по ключ:
:ets.lookup(:people, "pe60")
#=> [
#=> {"Петър", "Петров", "pe60", 55,
#=> ["Еврофутбол", "мачове", "ракийка и салатка",
#=> "Цеца"]}
#=> ]
:ets.lookup(:people, "meddle")
#=> []- Функцията
:ets.lookup/2винаги връща списък. - Това е така, защото при
:bagи:duplicate_bagтаблици има вероятност да имаме повече от един резултат. - Ако ни трябва само дадена колона за даден ключ можем да ползваме lookup_element.
- Забелязвате, че при ETS позициите започват от 1.
age = :ets.lookup_element(:people, "pe60", 4)
#=> 55
age = :ets.lookup_element(:people, "meddle", 4)
#=> ** (ArgumentError) argument error- Erlang има специален 'език' за намиране на данни.
- Използва се за селекция и се нарича 'match спецификация'.
- Една match-спецификация се състои от:
- Head : Описва редовете, които искаме да match-нем или изберем.
- Guard : Допълнителни филтри към тези редове-записи.
- Result : Как да изглежда резултата (Трансформации).
match_spec = [
{
{:"$1", :"$2", :_, :_, :"$3"}, # Head
[
{:andalso, {:is_list, :"$3"},
{:>, {:length, :"$3"}, 3}}
], # Guard
[{{:"$1", :"$2"}}] # Result
}
]
:ets.select(:people, match_spec)
#=> [{"Милена", "Стоева"}, {"Петър", "Петров"}]match_func =
fn {v1, v2, _, _, v3} when is_list(v3) and length(v3) > 3 ->
{v1, v2}
end
#=> #Function<6.99386804/1 in :erl_eval.expr/5>
:ets.fun2ms(match_func)
#=> [
#=> {{:"$1", :"$2", :_, :_, :"$3"},
#=> [{:andalso, {:is_list, :"$3"}, {:>, {:length, :"$3"}, 3}}],
#=> [{{:"$1", :"$2"}}]}
#=> ]:ets.match(:people, {:"$1", :"$2", :"_", :"_", :"_"})
#=> [["Милена", "Стоева"], ["Петър", "Петров"]]- Съпоставянето си е нормалното Elixir/Erlang съпоставяне, което означава, че можем да правим и такива неща:
:ets.match(:people, {:"_", :"_", :"$2", :"_", [:"$1" | :"_"]})
[["социални мрежи", "milen4it0"], ["Еврофутбол", "pe60"]]- ETS съдържа функции за обхождане на редовете на таблица като списък.
- При
:ordered_setтаблици редът е по ключовете, които са сортирани. - Ако таблицата е друг тип редът зависи от това как се пазят записите вътрешно.
- Функцията
:ets.first/1ще върне ключа на първия ред. - Съответно
:ets.last/1ще върне ключа на последния ред.
:ets.next(:people, :ets.first(:people))
#=> "pe60"
:ets.prev(:people, "pe60")
#=> "milen4it0"
:ets.next(:people, "pe60")
#=> :"$end_of_table"
:ets.prev(:people, "milen4it0")
#=> :"$end_of_table"- Могат да се задават стойности за които няма ключове в таблицата, те ще се сравнят и ще се намери най-близкия ключ:
:ets.prev(:people, "s")
#=> "pe60"
:ets.next(:people, "s")
#=> :"$end_of_table"- Лесен начин е да се използва
:ets.insert/2за вече съществуващ ключ, което ще презапише реда. - Има и
:ets.update_element/3- задава се таблица, ключ и наредена двойка от позиция за промяната и нова стойност.
:ets.update_element(:people, "pe60", {1, "Пешо"})
#=> true
:ets.lookup(:people, "pe60")
#=> [
#=> {"Пешо", "Петров", "pe60", 55,
#=> ["Еврофутбол", "мачове", "ракийка и салатка",
#=> "Цеца"]}
#=> ]- Има специализирани функции
update_counterза атомарно увеличаване или намаляване на брояч, пазен в таблицата. - Пример е следната имплементация на
RequestsPerUser:
defmodule RequestsPerUserETS do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
def init(_) do
{:ok, :ets.new(__MODULE__, [:named_table, :public])}
end
def update(user) do
:ets.update_counter(__MODULE__, user, {2, 1}, {user, 0})
end
def get(user) do
case :ets.match(__MODULE__, {user, :"$1"}) do
[[n]] -> n
[] -> 0
end
end
endRequestsPerUserETS.start_link()
#=> {:ok, #PID<0.115.0>}
RequestsPerUserETS.get("meddle")
#=> 0
RequestsPerUserETS.update("meddle")
#=> 1
RequestsPerUserETS.get("meddle")
#=> 1- Лесно можем да изтрием запис по ключ:
:ets.delete(:people, "pe60")
#=> true
:ets.info(:people, :size)
#=> 1- Или по съвпадение:
:ets.match_delete(:people, {"Милена", :"_", :"_", :"_", :"_"})
#=> true
:ets.info(:people, :size)
#=> 0- Можем и да освободим цялата таблица от паметта, когато поискаме:
:ets.delete(:people)
#=> true- Публични таблици могат да бъдат изтрити от който и да е процес.
- Ако таблица не е публична, тя може да бъде изтрита само от процесът, който я притежава.
- Ако процесът owner спре да съществува, ETS таблицата му е изчистена от паметта.
- Ако не искаме това да се случи, можем да зададем процес-наследник.
- Това може да стане в
:ets.new/2с опцията{:heir, pid, data}. - Това е причината, когато ETS таблицата е публична (например cache), да я стартираме в процес, който не извършва никаква друга работа.
defmodule Heir do
use GenServer
def transfer_data(table) do
GenServer.call(__MODULE__, {:transfer_data, table})
end
def init(_), do: {:ok, %{}}
def handle_info({:"ETS-TRANSFER", table, from, data}, state) do
IO.puts(
[
"#{inspect table} transfered from ",
"#{inspect from} to #{inspect self()}"
]
)
{:noreply, Map.put(state, table, data)}
end
def handle_call({:transfer_data, table}, _, state) do
{:reply, Map.get(state, table), state}
end
end{:ok, heir_pid} = Heir.start_link()
#=> {:ok, #PID<0.115.0>}
spawn(fn ->
:ets.new(
:some_table,
[:named_table, {:heir, heir_pid, "woo"}]
)
end)
#=> #PID<0.118.0>
#output: :some_table transfered from #PID<0.118.0> to #PID<0.115.0>:ets.info(:some_table, :owner)
#=> #PID<0.115.0>- Когато има зададен
:heir, ако процесът-собственик на таблица умре, таблицата не се изчиства от паметта. - Процесът-наследник получава съобщение от типа
{:"ETS-TRANSFER", име-или-референция-към-таблицата, pid-на-предишния-собственик, информация-зададена-при-определянето-на-наследник}.
- В момента, в който owner процесът спре да съществува, таблицата се притежава от heir процеса.
- Информацията, зададена при определянето на наследника, може да се използва за дебъг или история на наследяването.
- Ако не зададем наследник при създаване на таблицата, можем да го направим в последствие, използвайки
:ets.setopts(table, heir_tuple). - Възможно е да променим собственика на таблица и ръчно, дори когато процесът-собственик на таблицата още съществува.
- Това става с
:ets.give_away/3(:ets.give_away(table, heir_pid, transfer_data)). - По подразбиране наследника има стойност
:none. Винаги можем да премахнем наследник като зададем тази стойност.
- Можем да зададем опцията
:compressedна таблица. - Това означава, че всичко освен ключа на даден запис ще е компресирано.
- Това ще оптимизира паметта използвана от таблицата.
- Цената е, че четене на цели редове ще стане по-бавно.
- По-бавно ще стане и добавянето на нови записи.
- Има две опции за оптимизиране на често срещни случаи.
- И двете по подразбиране са изключени (
false).
read_concurrency: true- Ако имаме множество четения без писания често.- Ако го направим и често редуваме четения и писания, достъпът до таблицата ще се влоши.
write_concurrency: true- Ако имаме много промени.- Промени от типа на insert, delete.
- По-бързите конкурентни писания идват с цената на повече използвана памет.
- Писания едно след друго от един и същ процес се забавят.
- Борят на възможни конкурентни четения става по-ограничен.
- Можем да комбинираме двете опции ако имаме периоди на много четения и периоди на множество писания.
- Тези опции са свързани с начина на заключване и синхронизиране на достъпа до записите в таблиците.
- Най-добре е с наблюдение и тестване да определите стойностите на тези опции.
- Споделено състояние между процеси, които да имат конкурентен и бърз достъп до него.
- Пазене на текущото състояние на процес, което да бъде възстановено, ако процеса умре.
- Вместо Map или List за оптимизация, макар че не винаги това ще е най-бързото решение.
- За регистриране на познати процеси.
- Когато искаме да работим с данни, които могат да бъдат изпращани от един node към друг.
- Когато искаме дистрибутирана база данни върху множество node-ове.
- Когато искаме да запазим данните си между рестартирания на node-а си.
- В повечето случаи проста комуникация между процеси върши работа.
- При тестване, ако имаме нужда от временно състояние в паметта, по-добре да ползваме Agent.
- При мемоизация на някакъв резултат, докато си строим данни, които ще използваме по-късно в даден процес, по-добре да ползваме структура в heap-а на този процес.
- ETS има основната задача да пребори single-process bottleneck проблема. Ако данните ни живеят само в процеса, който ги ползва, или ако само един процес ги чете/пише, ETS не е необходима.
- По-бавен от ETS за всичко, с почти същия интерфейс.
- Но пък записва на диска. Затова е и по-бавен.
- Има
:auto_saveопция, която го flush-ва на 3 секунди по подразбиране. - Използва се когато не искаме да имаме загуба на state.
- Ако програмата ни излезе без crash, DETS ще се flush-не.
- Не поддържа
:ordered_setтаблици
- Ние не сме го ползвали в production.
- Най-големият му use-case е mnesia, но и там можем и да го заменим, да речем с rocksdb или leveled.
- DETS има ограничение за 2GB per file, при което mnesia разхвърля данни по файлове и става по бавна с по-големи бази.
- Mnesia е множество ETS таблици.
- Ако има запазване на диска, тогава се използва DETS.
- Mnesia има релационен/обектен хибриден модел. Релациите между ETS таблиците са имплементирани с други ETS таблици.
- Mnesia използва spec езика на ETS за заявки.
- Mnesia поддържа ACID (atomicity, consistency, isolation, durability) транзакции.
- Mnesia поддържа backend плъгини за storage (споменахме два), така че DETS може да бъде заместен на диска.
- Mnesia е дистрибутирана - може да имаме множество node-ове със споделени данни.
- Elixir wrapper за Mnesia: Amnesia
- Алтернатива на ETS е
:persistent_term, много прилича на:etsкато идея - споделена памет. - Разликата е, че е още по-оптимизиран за четене, но е много неефективен за писане/обновяване.
- Четенето (
get/1) винаги е O(1). Няма заключване и няма копиране на данни към процеса. - Писането (
put/2) е пропорционално на броя на всички Persitent Term-ове, защото всичко се копира, така работи тази памет. - Писанията и триенията почти винаги водят до GC на всички процеси рефериращи Persistent Term таблицата.
- Ако се изтрие терм, който се ползва от някой процес, то той бива записан в хийпа на този процес. Това може да доведе до използване на неочаквано голямо количество памет.
- По-добре да има малко големи Persistent Term-ове отколкото много малки.
- Добри са за shared конфигурация между процеси.
- Добри са за референции към дълго живеещи ресурси като NIF ресурси.
- Добри са за пазене на референции към ETS таблици и counter-и.
- Това е поредната оптимизация на много специални случаи.
- Локален map с бърз достъп във всеки процес.
- Често се ползва за пазене на metadata свързана с процеса.
- Не го ползвайте ако не знаете защо, вътре в самия процес е глобална стойност!
- Logger го ползва за metadata, IEX го ползва за history.
- Процес, стартиращ Task, го ползва - "$ancestors" ключа.
- Повече прочетете тук















