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

Concurrence

Une caractéristique phare de Elixir est son support de la concurrence. Grâce à la machine virtuelle de Erlang, BEAM, la programmation concurrente en Elixir est aisée. Le modèle de concurrence repose sur des Acteurs, c’est-à-dire des processus qui communiquent avec d’autres processus en échangeant des messages.

Dans cette lesson, nous passons en revue les modules d’Elixir dédiés à la programmation concurrente. Nous aborderons dans les chapitres suivants OTP, une plateforme qui en tire parti.

Processus

Les processus dans la machine virtuel de Erlang, BEAM, sont légers, et ils utilisent tous les cœurs du CPU. Bien qu’ils s’apparentent à des fils d’exécutions (en anglais : Threads) natifs, ils sont en réalité bien plus simples, et il n’est pas rare qu’une application Elixir compte des milliers de processus en parallèle.

Le moyen le plus simple de créer un processus est la fonction spawn ; elle lance une fonction, nommée ou anonyme, en parallèle du fil d’exécution principal. Cette fonction retourne l’identifiant du processus qu’elle crée, ou PID (de l’anglais : Process IDentifier), qui permet de l’identifier dans l’application.

À titre d’exemple, créons un module avec une fonction :

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

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

Pour évaluer cette fonction de manière asynchrone, nous utilisons spawn\3 :

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

Communication par messages

Pour communiquer, les processus échangent des messages.

Cela repose sur deux composants : send/2 et receive. La fonction send/2 permet d’envoyer un message à un processus en utilisant son PID. Pour écouter les messages, nous utilisons receive ; s’il n’y en a pas encore, le processus s’interrompt en attendant le prochain message correspondant à l’une des clauses.

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

Veuillez noter que listen/0 est récursive : cela permet au processus de traiter plusieurs messages. Sans récursion, le processus se serait achevé après avoir traité le premier message.

Liaison entre processus

Un problème avec spawn est qu’il n’est pas évident de savoir quand un processus s’interrompt. Pour cela, nous avons besoin de relier les processus entre avec spawn_link. Deux processus liés reçoivent des notifications d’extinction de l’un et de l’autre.

Par exemple :

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

Parfois, nous ne voulons pas qu’un processus s’interrompt en même temps que celui auquel il est lié. Pour cela, nous devons piéger l’ordre d’extinction avec Process.flag/2. Cela utilise la fonction process_flag/2 d’Erlang pour le flag trap_exit. Quand l’ordre d’extinction est piégé (c’est-à-dire que le flag trap_exit a pour valeur true), le signal d’extinction est recy comme un tuple : {: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

Surveillance d’un processus

Comment faire pour qu’un processus soit informé de l’état d’un autre processus sans pour autant les lier ? Nous pouvons utiliser spawn_monitor. Cela permet à un processus de surveiller un autre processus, et de recevoir un message quand il s’éteint, sans recevoir lui-même l’ordre de s’éteindre.

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

Agents

Les agents sont une abstraction de processus permettant de maintenir un état. Nous pouvons accéder au contenu d’un agent depuis d’autres processus.

Nous initialisons et mettons à jour l’état d’un agent comme suit :

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]

Nous pouvons donner un nom à un agent. Ce nom peut remplacer son 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]

Tâches

Une tâche est une fonction exécutée en tâche de fond et dont la valeur de retour est consultable ultérieurement. Cela peut être utile pour traiter des opérations longues sans bloquer l’application.

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>
}

# Do some work

iex> Task.await(task)
4000
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!