Elisir - Processi

In Elixir, tutto il codice viene eseguito all'interno dei processi. I processi sono isolati gli uni dagli altri, vengono eseguiti simultaneamente e comunicano tramite il passaggio di messaggi. I processi di Elixir non devono essere confusi con i processi del sistema operativo. I processi in Elixir sono estremamente leggeri in termini di memoria e CPU (a differenza dei thread in molti altri linguaggi di programmazione). Per questo motivo, non è raro avere decine o addirittura centinaia di migliaia di processi in esecuzione contemporaneamente.

In questo capitolo, impareremo i costrutti di base per generare nuovi processi, nonché per inviare e ricevere messaggi tra processi diversi.

La funzione spawn

Il modo più semplice per creare un nuovo processo è utilizzare il file spawnfunzione. Ilspawnaccetta una funzione che verrà eseguita nel nuovo processo. Ad esempio:

pid = spawn(fn -> 2 * 2 end)
Process.alive?(pid)

Quando il programma di cui sopra viene eseguito, produce il seguente risultato:

false

Il valore di ritorno della funzione spawn è un PID. Questo è un identificatore univoco per il processo e quindi se esegui il codice sopra il tuo PID, sarà diverso. Come puoi vedere in questo esempio, il processo è morto quando controlliamo per vedere se è vivo. Questo perché il processo terminerà non appena avrà terminato di eseguire la funzione data.

Come già accennato, tutti i codici Elixir vengono eseguiti all'interno dei processi. Se esegui la funzione self vedrai il PID per la tua sessione corrente -

pid = self
 
Process.alive?(pid)

Quando il programma di cui sopra viene eseguito, produce il seguente risultato:

true

Passaggio del messaggio

Possiamo inviare messaggi a un processo con send e riceverli con receive. Passiamo un messaggio al processo in corso e riceviamolo sullo stesso.

send(self(), {:hello, "Hi people"})

receive do
   {:hello, msg} -> IO.puts(msg)
   {:another_case, msg} -> IO.puts("This one won't match!")
end

Quando il programma di cui sopra viene eseguito, produce il seguente risultato:

Hi people

Abbiamo inviato un messaggio al processo corrente utilizzando la funzione di invio e lo abbiamo passato al PID di self. Quindi abbiamo gestito il messaggio in arrivo utilizzando ilreceive funzione.

Quando un messaggio viene inviato a un processo, il messaggio viene archiviato nel file process mailbox. Il blocco di ricezione passa attraverso la cassetta postale del processo corrente alla ricerca di un messaggio che corrisponda a uno qualsiasi dei modelli forniti. Il blocco di ricezione supporta guardie e molte clausole, come case.

Se non è presente alcun messaggio nella cassetta postale che corrisponde a nessuno dei modelli, il processo corrente attenderà fino all'arrivo di un messaggio corrispondente. È anche possibile specificare un timeout. Per esempio,

receive do
   {:hello, msg}  -> msg
after
   1_000 -> "nothing after 1s"
end

Quando il programma di cui sopra viene eseguito, produce il seguente risultato:

nothing after 1s

NOTE - È possibile impostare un timeout pari a 0 quando ci si aspetta già che il messaggio sia nella casella di posta.

Collegamenti

La forma più comune di generazione delle uova in Elixir è in realtà via spawn_linkfunzione. Prima di dare un'occhiata a un esempio con spawn_link, capiamo cosa succede quando un processo fallisce.

spawn fn -> raise "oops" end

Quando il programma sopra viene eseguito, produce il seguente errore:

[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
   :erlang.apply/2

Ha registrato un errore ma il processo di spawn è ancora in esecuzione. Questo perché i processi sono isolati. Se vogliamo che l'errore in un processo si propaghi a un altro, dobbiamo collegarli. Questo può essere fatto conspawn_linkfunzione. Consideriamo un esempio per capire lo stesso:

spawn_link fn -> raise "oops" end

Quando il programma sopra viene eseguito, produce il seguente errore:

** (EXIT from #PID<0.41.0>) an exception was raised:
   ** (RuntimeError) oops
      :erlang.apply/2

Se stai eseguendo questo file iexshell quindi la shell gestisce questo errore e non esce. Ma se esegui prima creando un file di script e poi usandoelixir <file-name>.exs, anche il processo genitore verrà interrotto a causa di questo errore.

Processi e collegamenti svolgono un ruolo importante nella creazione di sistemi a tolleranza di errore. Nelle applicazioni Elixir, spesso colleghiamo i nostri processi a supervisori che rileveranno quando un processo muore e inizierà un nuovo processo al suo posto. Questo è possibile solo perché i processi sono isolati e non condividono nulla per impostazione predefinita. E poiché i processi sono isolati, non è possibile che un errore in un processo si blocchi o danneggi lo stato di un altro. Mentre altre lingue ci richiederanno di catturare / gestire le eccezioni; in Elixir, in realtà stiamo bene lasciando che i processi falliscano perché ci aspettiamo che i supervisori riavviino correttamente i nostri sistemi.

Stato

Se stai creando un'applicazione che richiede uno stato, ad esempio, per mantenere la configurazione dell'applicazione, o devi analizzare un file e tenerlo in memoria, dove lo memorizzerai? La funzionalità di processo di Elixir può tornare utile quando si fanno queste cose.

Possiamo scrivere processi che ripetono all'infinito, mantengono lo stato e inviano e ricevono messaggi. Ad esempio, scriviamo un modulo che avvia nuovi processi che funzionano come un archivio di valori-chiave in un file denominatokv.exs.

defmodule KV do
   def start_link do
      Task.start_link(fn -> loop(%{}) end)
   end

   defp loop(map) do
      receive do
         {:get, key, caller} ->
         send caller, Map.get(map, key)
         loop(map)
         {:put, key, value} ->
         loop(Map.put(map, key, value))
      end
   end
end

Nota che il start_link la funzione avvia un nuovo processo che esegue il file loopfunzione, iniziando con una mappa vuota. Illoopla funzione attende quindi i messaggi ed esegue l'azione appropriata per ogni messaggio. Nel caso di un file:getmessaggio, invia un messaggio al chiamante e chiama nuovamente il loop, in attesa di un nuovo messaggio. Mentre il:put il messaggio effettivamente invoca loop con una nuova versione della mappa, con la chiave e il valore dati memorizzati.

Eseguiamo ora quanto segue:

iex kv.exs

Ora dovresti essere nel tuo file iexconchiglia. Per testare il nostro modulo, prova quanto segue:

{:ok, pid} = KV.start_link

# pid now has the pid of our new process that is being 
# used to get and store key value pairs 

# Send a KV pair :hello, "Hello" to the process
send pid, {:put, :hello, "Hello"}

# Ask for the key :hello
send pid, {:get, :hello, self()}

# Print all the received messages on the current process.
flush()

Quando il programma di cui sopra viene eseguito, produce il seguente risultato:

"Hello"