Subscribe now

Processes and Messaging [02.13.2017]

Concurrency is one of the primary draws to Elixir and Erlang. Concurrency is achieved via Processes. Let's have a look at spawning processes and sending messages between them.

Project

We'll start a new project:

mix new ping_pong
cd ping_pong

Now let's start out with a test:

vim test/ping_pong_test.exs

We'll test that we can spawn a process, send it a ping message, and get a message back.

defmodule PingPongTest do
  use ExUnit.Case
  doctest PingPong

  test "it responds to a pong with a ping" do
    # spawn takes a module, a function name, and a list of arguments
    # It starts a new process, running that function. When the function
    # completes, the new process will die.
    ping = spawn(Ping, :start, [])
    # send lets you send messages to a process
    # self provides the current process's PID, or Process ID
    send(ping, {:pong, self()})
    # We'll assert that when we send a pong to a process, we receive back a ping
    # This waits for up to 100ms and passes if the message is received in that
    # time frame, failing if it isn't.
    assert_receive {:ping, ^ping}
  end
end

OK, so we can try to run this:

mix test

It will fail because there's no Ping module with a start function. We can make that module:

vim lib/ping.ex
defmodule Ping do
  def start do
    :ok
  end
end

If you run the test now, it will fail because it never receives a message back. This is because our process doesn't respond to those messages, of course. To listen for a message, we can use receive. We'll listen for a pong message, capture the process id that we're sent, and send a ping message back.

defmodule Ping do
  def start do
    # receive pattern matches on a series of potential messages and runs some
    # code when it receives that message. Here we'll just send a message to the
    # pid we're sent.
    receive do
      {:pong, from} -> send(from, {:ping, self()})
    end
  end
end

Now this works and we can send this message, and our tests pass. But if we were to send another message, we would get no response. That's because after our ping process has received a message, the function is complete and the process dies. Let's write a test that we can send two messages:

  test "it responds to two messages" do
    ping = spawn(Ping, :start, [])
    send(ping, {:pong, self()})
    assert_receive {:ping, ^ping}
    send(ping, {:pong, self()})
    assert_receive {:ping, ^ping}
  end

Next, we will extract this function into a loop function and have loop call itself via recursion:

defmodule Ping do
  def start do
    loop()
  end

  def loop do
    receive do
      {:pong, from} -> send(from, {:ping, self()})
    end
    loop()
  end
end

Now, it will respond to the first message and then call loop again. If we run the test, we'll see that it passes.

It's worth talking a little bit about how receive works. Processes have a mailbox, and any messages sent to a process queue up in a list in the mailbox. receive will look at the mailbox, and handle the first message it finds in the order specified in the call to receive. If there are no messages, it blocks until there is a message.

Grand Finale

For the grand finale, let's add one more message to this - we will make Ping handle a :ping message as well, responding with :pong in that case. Then we will output to the console each time a message is received, and we'll start two processes talking to one another:

defmodule Ping do
  def start do
    loop()
  end

  def loop do
    receive do
      {:pong, from} ->
        IO.puts "ping ->"
        :timer.sleep 500
        send(from, {:ping, self()})
      {:ping, from} ->
        IO.puts "            <- pong"
        :timer.sleep 500
        send(from, {:pong, self()})
    end
    loop()
  end
end

Now we'll start one of each and get them talking to each other.

iex -S mix
iex(1)> ping = spawn(Ping, :start, [])
#PID<0.108.0>
iex(2)> pong = spawn(Ping, :start, [])
#PID<0.110.0>
iex(3)> send(ping, {:pong, pong})
ping ->
{:pong, #PID<0.110.0>}
            <- pong
ping ->
            <- pong
ping ->
            <- pong
ping ->
            <- pong
...

And now we can see them talking to one another.

Summary

In today's episode, we learned what processes were, how to spawn them, and how to communicate with them. We did it with tests, introducing assert_receive.

I hope you enjoyed it. See you soon!

Resources