Do you want to pick up from where you left of?
Take me there

Συγχρονισμός

Ένα από τα σημαντικά σημεία της Elixir είναι η υποστήριξή της για συγχρονισμό. Χάρη στην Εικονική Μηχανή της Erlang (BEAM), ο συγχρονισμός στην Elixir είναι πιο εύκολος από το αναμενόμενο. Το μοντέλο συγχρονισμού στηρίζεται στους Ηθοποιούς, μια περιορισμένη διεργασία που επικοινωνεί με άλλες διεργασίες μέσω αποστολής μηνυμάτων.

Σε αυτό το μάθημα θα δούμε τις ενότητες συγχρονισμού που έρχονται με την Elixir Στο επόμενο κεφάλαιο θα καλύψουμε τις συμπεριφορές OTP που τις υλοποιούν.

Διεργασίες

Οι διεργασίες στην εικονική μηχανή της Erlang είναι ελαφριές και τρέχουν σε όλους τους επεξεργαστές. Παρόλο που φαίνονται σαν τοπικά νήματα, είναι πιο απλές και είναι συνηθισμένο να έχουμε χιλιάδες συγχρονισμένες διεργασίες σε μια εφαρμογή Elixir.

Ο πιο απλός τρόπος να δημιουγήσουμε μια νέα διεργασία είναι η spawn, η οποία δέχεται είτε μια ανώνυμη ή μια ονομασμένη συνάρτηση. Όταν δημιουργούμε μια νέα διεργασία επιστρέφει ένα Προσδιοριστικό Διεργασίας, ή PID για να την προσδιορίζει μοναδικά στην εφαρμογή μας.

Για να ξεκινήσουμε θα δημιουργήσουμε μια ενότητα και θα ορίσουμε μια συνάρτηση που θα θέλαμε να τρέξουμε:

defmodule Example do
  def add(a, b) do
    IO.puts(a + b)
  end
end

iex> Example.add(2, 3)
5
:ok

Για να τρέξουμε τη συνάρτηση ασύγχρονα θα χρησιμοποιήσουμε την spawn/3:

iex> spawn(Example, :add, [2, 3])
5
#PID<0.80.0>

Αποστολή Μηνυμάτων

Για να επικοινωνήσουν, οι διεργασίες στηρίζονται στο πέρασμα μηνυμάτων. Υπάρχουν δύο κύρια συστατικά σε αυτό: οι send/2 και receive. Η συνάρτηση send/2 μας επιτρέπει να στέλνουμε μηνύματα σε PID. Για να τα λαμβάνουμε χρησιμοποιούμε την receive για να αντιπαραβάλουμε μηνύματα. Αν δεν βρεθεί ταίρι η εκτέλεση συνεχίζει απερίσπαστη.

defmodule Example do
  def listen do
    receive do
      {:ok, "hello"} -> IO.puts("World")
    end

    listen()
  end
end

iex> pid = spawn(Example, :listen, [])
#PID<0.108.0>

iex> send pid, {:ok, "hello"}
World
{:ok, "hello"}

iex> send pid, :ok
:ok

Μπορεί να παρατηρήσετε ότι η συνάρτηση ‘listen/0’ είναι αναδρομική, αυτό επιτρέπει στην διεργασία μας να διαχειρίζεται πολλαπλά μηνύματα. Χωρίς την αναδρομή η διεργασία μας θα τερματιζόταν μετά την διαχείριση του πρώτου μηνύματος.

Σύνδεση Διεργασιών

Ένα πρόβλημα την spawn είναι όταν πρέπει να ξέρουμε πότε μια διεργασία κρασάρει. Για αυτό πρέπει να συνδέσουμε τις διεργασίες μας με την χρήση της spawn_link. Δύο συνδεδεμένες διεργασίες θα λάβουν ειδοποιήσεις εξόδου η μία από την άλλη:

defmodule Example do
  def explode, do: exit(:kaboom)
end

iex> spawn(Example, :explode, [])
#PID<0.66.0>

iex> spawn_link(Example, :explode, [])
** (EXIT from #PID<0.57.0>) evaluator process exited with reason: :kaboom

Μερικές φορές δεν θέλουμε τις συνδεδεμένες διεργασίες μας να κρασάρουν την τρέχουσα. Για αυτό πρέπει να παγιδεύσουμε τις εξόδους. Όταν παγιδεύουμε τις εξόδους αυτές θα ληφθούν σαν ένα μήνυμα με μορφή τούπλας: {:EXIT, from_pid, reason}.

defmodule Example do
  def explode, do: exit(:kaboom)

  def run do
    Process.flag(:trap_exit, true)
    spawn_link(Example, :explode, [])

    receive do
      {:EXIT, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end

iex> Example.run
Exit reason: kaboom
:ok

Παρακολούθηση Διεργασιών

Τι γίνεται όταν δεν θέλουμε να συνδέσουμε δύο διεργασίες αλλά παρόλα αυτά να ενημερωνόμαστε; Για αυτό μπορούμε να χρησιμοποιήσουμε την παρακολούθηση διεργασίας με την spawn_monitor. Όταν παρακολουθούμε μια διεργασία λαμβάνουμε ένα μήνυμα αν η διεργασία κρασάρει χωρίς να κρασάρει η τρέχουσα διεργασία ή να παγιδεύσουμε τις εξόδους.

defmodule Example do
  def explode, do: exit(:kaboom)

  def run do
    spawn_monitor(Example, :explode, [])

    receive do
      {:DOWN, _ref, :process, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end

iex> Example.run
Exit reason: kaboom
:ok

Πράκτορες

Οι πράκτορες είναι μια αφαίρεση γύρω από την διατήρηση κατάστασης των διεργασιών παρασκηνίου. Μπορούμε να έχουμε πρόσβαση σε αυτές από άλλες διεργασίες μέσα από την εφαρμογή και τον κόμβο μας. Η κατάσταση του Πράκτορά μας ορίζεται σαν η τιμή επιστροφής της συνάρτησής μας:

iex> {:ok, agent} = Agent.start_link(fn -> [1, 2, 3] end)
{:ok, #PID<0.65.0>}

iex> Agent.update(agent, fn (state) -> state ++ [4, 5] end)
:ok

iex> Agent.get(agent, &(&1))
[1, 2, 3, 4, 5]

Όταν ονομάζουμε έναν Πράκτορα μπορούμε να αναφερθούμε σε αυτόν αντί στο PID του:

iex> Agent.start_link(fn -> [1, 2, 3] end, name: Numbers)
{:ok, #PID<0.74.0>}

iex> Agent.get(Numbers, &(&1))
[1, 2, 3]

Εργασίες

Οι Εργασίες παρέχουν ένα τρόπο να εκτελέσουμε μια συνάρτηση στο παρασκήνιο και να λάβουμε την τιμή επιστροφής της αργότερα. Μπορούν να είναι ιδιαίτερα χρήσιμες όταν χειρίζονται ακριβές λειτουργίες χωρίς να μπλοκάρουν την εκτέλεση της εφαρμογής.

defmodule Example do
  def double(x) do
    :timer.sleep(2000)
    x * 2
  end
end

iex> task = Task.async(Example, :double, [2000])
%Task{
  owner: #PID<0.105.0>,
  pid: #PID<0.114.0>,
  ref: #Reference<0.2418076177.4129030147.64217>
}

# Κάντε κάτι άλλο

iex> Task.await(task)
4000
Έπιασες λάθος ή θέλεις να συνεισφέρεις στο μάθημα; Επεξεργαστείτε αυτό το μάθημα στο GitHub!