Lesson 10 / 25
Processes and Message Passing
Spawn processes, send and receive messages, and link or monitor them.
The actor model on the BEAM
A BEAM process is an isolated unit of execution with its own heap and mailbox; it is not an OS thread, and creating one takes microseconds and a few kilobytes. spawn/1 starts a process running a function and returns its PID. Processes communicate only by messages: send(pid, message) is asynchronous and copies the message into the receiver's mailbox, and receive pattern-matches messages from the mailbox, optionally with an after timeout. Because nothing is shared, there are no locks or data races. A process keeps state by calling itself recursively with new arguments in a receive loop. Links (spawn_link) tie two processes together so that if one crashes, the other receives an exit signal and normally crashes too, which supervisors use to detect failures. Monitors (Process.monitor) are one-way: the watcher receives a :DOWN message instead of crashing. Processes can be registered under a name. The BEAM schedules processes preemptively, so one busy process cannot freeze the others, which keeps latency predictable.
Processes and mailboxes
Each process has a private mailbox; messages are copied, never shared.
A stateful counter process
A recursive receive loop holds state; links and monitors report failures.
defmodule Counter do
def start(initial \\ 0), do: spawn(fn -> loop(initial) end)
defp loop(count) do
receive do
{:increment, by} ->
loop(count + by) # new state via recursion
{:get, caller} ->
send(caller, {:count, count})
loop(count)
:stop ->
:ok # returning ends the process
end
end
end
pid = Counter.start()
send(pid, {:increment, 5})
send(pid, {:increment, 2})
send(pid, {:get, self()})
receive do
{:count, value} -> IO.puts("count = #{value}") # count = 7
after
1_000 -> IO.puts("no reply")
end
# monitoring: get a :DOWN message instead of crashing
worker = spawn(fn -> raise "boom" end)
ref = Process.monitor(worker)
receive do
{:DOWN, ^ref, :process, ^worker, reason} -> IO.puts("worker died: #{inspect(reason)}")
endAlways plan for unmatched messages
Messages that no receive clause matches stay in the mailbox forever, slowing every future receive. GenServer handles this for you; in hand-written loops, add a catch-all clause or use timeouts.
Quick check: How do BEAM processes share data?
- Through shared memory and locks
- Through global variables
- They do not share memory; they communicate by copying messages into mailboxes
- Through files only
Answer
They do not share memory; they communicate by copying messages into mailboxes — Isolation plus message passing eliminates data races.