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"