Mike Logaciuk

Podstawy języka Elixir (część trzecia)

09 Oct 2024

header

Spis treści

Procesy

Cały kod Elixira działa wewnątrz procesów, które są od siebie odizolowane i działają współbieżnie tudzież równolegle, a komunikują się poprzez przekazywanie wiadomości.

Procesy te nie mogą być mylone z procesami w systemie operacyjnym.

Te są niesamowicie lekkie w rozumieniu zużycia procesora (ang. CPU) jak i pamięci (nawet w porównaniu do wątków w innych językach programowania).

Sprawdźmy więc jak wyglądają podstawowe konstrukty w tym obszarze (tworzenia procesów) oraz jak wygląda przekazywanie wiadomości między nimi.

Tworzenie procesów

Wcześniej stworzyłem funkcję do generowania ID na potrzeby zamówień Order.Helpers.generate_id/1:

Order.Helpers.generate_id

Aby uruchomić ją jako proces, wystarczy użyć polecenia spawn, które zwraca PID procesu:

gen_id_pid = spawn(Order.Helpers, :generate_id, [])

Aby sprawdzić czy proces jeszcze żyje możemy sprawdzić z pomocą:

Process.alive?(gen_id_pid)

Aby sprawdzić PID aktualnego procesu, używamy self/0:

self()

Możemy też zespawnować proces do zwykłej anonimowej funkcji:

fnxp = fn -> IO.inspect("I am the lucky phrase #{Order.Helpers.generate_id()}") end
fnxp_process = spawn(fnxp)
Process.alive?(fnxp_process)

Przekazywanie wiadomości

Wiadomości w przypadku procesów wysyłamy z pomocą send/2, a odbieramy przy pomocy receive/1:

parent_pid = self()
spawn(fn -> send(parent_pid, {:hi, self()}) end)
receive do
  {:hi, pid} -> "Got a #{:hi} from #{inspect pid}"
end

Inny przykład

Prosty producent/konsumer

Innym ciekawym przykładem może być prosty producent/konsumer (ang. pub/sub), używający czystego Elixira:

defmodule Producer do
  def start_link do
    spawn_link(fn -> loop() end)
  end
  defp loop do
    receive do
      {:produce, consumer_pid} ->
        produced_item = :rand.uniform(1000)
        send(consumer_pid, {:produced_item, produced_item})
        loop()
    end
  end
end

defmodule Consumer do
  def start_link do
    spawn_link(fn -> loop() end)
  end
  defp loop do
    receive do
      {:produced_item, item} ->
        IO.puts("Consumed item: #{item}")
        loop()
    end
  end
end
defmodule ProducerConsumerExample do
  def run do
    consumer_pid = Consumer.start_link()
    producer_pid = Producer.start_link()

    send(producer_pid, {:produce, consumer_pid})
    send(producer_pid, {:produce, consumer_pid})
    :timer.sleep(1000)

    send(producer_pid, {:produce, consumer_pid})
    send(producer_pid, {:produce, consumer_pid})
    :timer.sleep(2000)

  end
end

ProducerConsumerExample.run()

Procesy zlinkowane

Bardzo rzadko jednak, używamy procesów w takiej jak powyżej formie - no chyba, że chcemy aby coś się wykonało w tle i aby ew. błąd osiągnięty tam, nie miał jakiegokolwiek wpływu na to co robi Nasz główny kod.

Jeżeli chcemy aby błąd w jednym procesie miał wpływ na następny - powinniśmy je zlinkować z pomocą spawn_link/1.

Task

Kolejnym ciekawym zagadnieniem jest istnienie Task‘ów w Elixirze, które zamiast spawn/1 oraz spawn_link/1 udostępniają Task.start/1 oraz Task.start_link/1, które w domyślne zamiast samego PID zwracają: {:ok, pid}.

To umożliwia używanie task‘ów w tzw supervision trees, udostępniając dodatkowo programiście Task.async/1 oraz Task.await/1.

Natomiast bardzo rzadko będziecie tak naprawdę używać funkcjonalności procesów i tasków bezpośrednio.

Agent

Ku temu stworzono Agents, które jako abstrakcje w okół stanu wchodzą w skład mechanizmów OTP.

Dla przykładu:

{:ok, pid} = Agent.start_link(fn -> %{} end) # {:ok, #PID<0.12.0>}
Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
Agent.get(pid, fn map -> Map.get(map, :hello) end)

Możemy też tego agenta oczywiście zatrzymać:

Agent.stop(pid)

Dodatkowo możemy nadać mu nazwę z pomocą :atom'u:

{:ok, store} = Agent.start_link(fn -> %{} end, name: :store) # {:ok, #PID<0.43.0>}

Natomiast… Nie jest to najlepsza praktyka, zwłaszcza w programach, które mogą mięć setki agentów. Po drugie atomy nie są odśmiecane, więc możemy dosyć sprawnie zapełnić pamięć mając np. miliony atomów tworzonych np. per proces.

W tym przypadku lepszym rozwiązaniem będzie użycie GenServer oraz Supervisor‘a.

Podsumowanie

To by było na tyle w tej części (krótszej niż pozostałe), kontynuacja oczywiście znajdzie się w kolejnym artykule.

Natomiast wiele ciekawych rzeczy nt. agent’ów, GenServer oraz Supervisor, znajdziecie także: tutaj.

Referencje