Who gets told when a process dies? By default, no one. Obviously the VM knows and can report it to the console, but your code will be oblivious unless you explicitly tell Elixir you want to get involved. Here’s the default case: we spawn a function that uses the Erlang timer library to sleep for 500 ms. It then exits with a status of :boom. The code that spawns it sits in a receive. If it receives a message, it reports that fact; otherwise, after one second it lets us know that nothing happened.
| defmodule Link1 do |
| import :timer, only: [ sleep: 1 ] |
| |
| def sad_function do |
| sleep 500 |
| exit(:boom) |
| end |
| def run do |
| spawn(Link1, :sad_function, []) |
| receive do |
| msg -> |
| IO.puts "MESSAGE RECEIVED: #{inspect msg}" |
| after 1000 -> |
| IO.puts "Nothing happened as far as I am concerned" |
| end |
| end |
| end |
| |
| Link1.run |
(Think about how you’d have written this in your old programming language.)
We can run this from the console:
| $ elixir -r link1.exs |
| Nothing happened as far as I am concerned |
The top level got no notification when the spawned process exited.
If we want two processes to share in each other’s pain, we can link them. When processes are linked, each can receive information when the other exits. The spawn_link call spawns a process and links it to the caller in one operation.
| defmodule Link2 do |
| import :timer, only: [ sleep: 1 ] |
| |
| def sad_function do |
| sleep 500 |
| exit(:boom) |
| end |
| def run do |
» | spawn_link(Link2, :sad_function, []) |
| receive do |
| msg -> |
| IO.puts "MESSAGE RECEIVED: #{inspect msg}" |
| after 1000 -> |
| IO.puts "Nothing happened as far as I am concerned" |
| end |
| end |
| end |
| |
| Link2.run |
The runtime reports the abnormal termination:
| $ elixir -r link2.exs |
| ** (EXIT from #PID<0.73.0>) :boom |
So our child process died, and it killed the entire application. That’s the default behavior of linked processes—when one exits abnormally, it kills the other.
What if you want to handle the death of another process? Well, you probably don’t want to do this. Elixir uses the OTP framework for constructing process trees, and OTP includes the concept of process supervision. An incredible amount of effort has been spent getting this right, so I recommend using it most of the time. (We cover this in Chapter 18, OTP: Supervisors.)
However, you can tell Elixir to convert the exit signals from a linked process into a message you can handle. Do this by trapping the exit.
| defmodule Link3 do |
| import :timer, only: [ sleep: 1 ] |
| |
| def sad_function do |
| sleep 500 |
| exit(:boom) |
| end |
| def run do |
» | Process.flag(:trap_exit, true) |
| spawn_link(Link3, :sad_function, []) |
| receive do |
| msg -> |
| IO.puts "MESSAGE RECEIVED: #{inspect msg}" |
| after 1000 -> |
| IO.puts "Nothing happened as far as I am concerned" |
| end |
| end |
| end |
| |
| Link3.run |
This time we see an :EXIT message when the spawned process terminates:
| $ elixir -r link3.exs |
| MESSAGE RECEIVED: {:EXIT, #PID<0.78.0>, :boom} |
It doesn’t matter why a process exits—it may simply finish processing, it may explicitly exit, or it may raise an exception—the same :EXIT message is received. Following an error, however, it contains details of what went wrong.
Linking joins the calling process and another process—each receives notifications about the other. By contrast, monitoring lets a process spawn another and be notified of its termination, but without the reverse notification—it is one-way only.
When you monitor a process, you receive a :DOWN message when it exits or fails, or if it doesn’t exist.
You can use spawn_monitor to turn on monitoring when you spawn a process, or you can use Process.monitor to monitor an existing process. However, if you use Process.monitor (or link to an existing process), there is a potential race condition—if the other process dies before your monitor call completes, you may not receive a notification. The spawn_link and spawn_monitor versions are atomic, however, so you’ll always catch a failure.
| defmodule Monitor1 do |
| import :timer, only: [ sleep: 1 ] |
| |
| def sad_function do |
| sleep 500 |
| exit(:boom) |
| end |
| def run do |
» | res = spawn_monitor(Monitor1, :sad_function, []) |
| IO.puts inspect res |
| receive do |
| msg -> |
| IO.puts "MESSAGE RECEIVED: #{inspect msg}" |
| after 1000 -> |
| IO.puts "Nothing happened as far as I am concerned" |
| end |
| end |
| end |
| |
| Monitor1.run |
Run it, and the results are similar to the spawn_link version:
| $ elixir -r monitor1.exs |
| {#PID<0.78.0>, #Reference<0.1328...>} |
| MESSAGE RECEIVED: {:DOWN, #Reference<0.1328...>, :process, |
| #PID<0.78.0>, :boom} |
(The Reference record in the message is the identity of the monitor that was created. The spawn_monitor call also returns it, along with the PID.)
So, when do you use links and when should you choose monitors?
It depends on your processes’ semantics. If the intent is that a failure in one process should terminate another, then you need links. If instead you need to know when some other process exits for any reason, choose monitors.