Processes and Messaging

Elixir processes, spawn, send/receive, links, monitors, and the actor model

Elixir runs on the BEAM (Erlang VM) and uses lightweight processes (not OS threads) for concurrency. Processes communicate by sending messages.

Spawning processes

# Spawn a process
pid = spawn(fn -> IO.puts("Hello from process") end)

# Spawn with a registered name
pid = spawn(fn -> loop() end)
Process.register(pid, :my_worker)

# Spawn linking (dies together)
spawn_link(fn -> raise "boom" end)

# Spawn monitoring (get notified on exit)
spawn_monitor(fn -> raise "boom" end)

Sending and receiving messages

# Send a message
send(pid, {:hello, "world"})
send(:my_worker, {:hello, "world"})

# Receive a message (blocks until a match)
receive do
  {:hello, name} -> IO.puts("Hello #{name}")
  {:error, reason} -> IO.puts("Error: #{reason}")
after
  5000 -> IO.puts("Timed out")
end

Flush mailbox

# View and remove all messages in the current process mailbox
flush()

Process info

Process.alive?(pid)               # => true | false
Process.info(pid, :message_queue_len)  # => {:message_queue_len, 0}
Process.info(pid, :links)         # => {:links, []}
self()                            # => current process PID

If a linked process crashes, the other process also crashes.

spawn_link(fn -> raise "boom" end)  # kills both processes
Process.link(pid)                    # link to an existing process
Process.unlink(pid)                  # remove link

Monitors (unidirectional)

Get a message when a process exits, without crashing.

ref = Process.monitor(pid)
# When pid exits, you receive:
#   {:DOWN, ref, :process, pid, reason}

Process.demonitor(ref)              # stop monitoring
Process.demonitor(ref, [:flush])    # stop + flush existing DOWN messages

State with processes

A common pattern: a process that maintains state by recursively looping and receiving messages.

defmodule Counter do
  def start(initial \\ 0) do
    spawn(fn -> loop(initial) end)
  end

  def increment(pid), do: send(pid, :inc)
  def decrement(pid), do: send(pid, :dec)
  def get(pid), do: send(pid, {:get, self()})

  defp loop(count) do
    receive do
      :inc -> loop(count + 1)
      :dec -> loop(count - 1)
      {:get, from} ->
        send(from, {:count, count})
        loop(count)
    end
  end
end

pid = Counter.start()
Counter.increment(pid)
Counter.increment(pid)
Counter.get(pid)
# receive do {:count, n} -> n end  => 2

Process dictionary

Each process has a local key-value store. Use sparingly — prefer passing state explicitly.

Process.put(:key, "value")
Process.get(:key)          # => "value"
Process.get_keys()         # => [:key]
Process.delete(:key)

Exiting and signals

# Normal exit
Process.exit(pid, :normal)     # process continues (doesn't kill)

# Kill (untrappable)
Process.exit(pid, :kill)       # process dies immediately

# Abnormal exit
Process.exit(pid, :shutdown)   # process dies

# Trap exits in a process
Process.flag(:trap_exit, true)
# Now you receive {:EXIT, pid, reason} messages instead of crashing