Here’s a module that defines a function we’d like to run as a separate process:
| defmodule SpawnBasic do |
| def greet do |
| IO.puts "Hello" |
| end |
| end |
Yup, that’s it. There’s nothing special—it’s just regular code.
Let’s fire up IEx and play:
| iex> c("spawn-basic.ex") |
| [SpawnBasic] |
First let’s call it as a regular function:
| iex> SpawnBasic.greet |
| Hello |
| :ok |
Now let’s run it in a separate process:
| iex> spawn(SpawnBasic, :greet, []) |
| Hello |
| #PID<0.42.0> |
The spawn function kicks off a new process. It comes in many forms, but the two simplest ones let you run an anonymous function and run a named function in a module, passing a list of arguments. (We used the latter here.)
The spawn returns a process identifier, normally called a PID. This uniquely identifies the process it creates. (This identifier could be unique among all processes in the world, but here it’s just unique in our application.)
When we call spawn, it creates a new process to run the code we specify. We don’t know exactly when it will execute—only that it is eligible to run.
In this example, we can see that our function ran and output “Hello” prior to IEx reporting the PID returned by spawn. But you can’t rely on this. Instead you’ll use messages to synchronize your processes’ activity.
Let’s rewrite our example to use messages. The top level will send greet a message containing a string, and the greet function will respond with a greeting containing that message.
In Elixir we send a message using the send function. It takes a PID and the message to send (an Elixir value, which we also call a term) on the right. You can send anything you want, but most Elixir developers seem to use atoms and tuples.
We wait for messages using receive. In a way, this acts the same as case, with the message body as the parameter. Inside the block associated with the receive call, you can specify any number of patterns and associated actions. Just as with case, the action associated with the first pattern that matches the function is run.
Here’s the updated version of our greet function.
| defmodule Spawn1 do |
| def greet do |
| receive do |
| {sender, msg} -> |
| send sender, { :ok, "Hello, #{msg}" } |
| end |
| end |
| end |
| |
| # here's a client |
| pid = spawn(Spawn1, :greet, []) |
| send pid, {self(), "World!"} |
| |
| receive do |
| {:ok, message} -> |
| IO.puts message |
| end |
The function uses receive to wait for a message, and then matches the message in the block. In this case, the only pattern is a two-element tuple, where the first element is the original sender’s PID and the second is the message. In the corresponding action, we use send sender, ... to send a formatted string back to the original message sender. We package that string into a tuple, with :ok as its first element.
Outside the module, we call spawn to create a process, and send it a tuple:
| send pid, { self, "World!" } |
The function self returns its caller’s PID. Here we use it to pass our PID to the greet function so it will know where to send the response.
We then wait for a response. Notice that we do a pattern match on {:ok, message}, extracting the second element of the tuple, which contains the actual text.
We can run this in IEx:
| iex> c("spawn1.exs") |
| Hello, World! |
| [Spawn1] |
Very cool. The text was sent, and greet responded with the full greeting.
Let’s try sending a second message.
| defmodule Spawn2 do |
| def greet do |
| receive do |
| {sender, msg} -> |
| send sender, { :ok, "Hello, #{msg}" } |
| end |
| end |
| end |
| |
| # here's a client |
| pid = spawn(Spawn2, :greet, []) |
| |
| send pid, {self(), "World!"} |
| |
| receive do |
| {:ok, message} -> |
| IO.puts message |
| end |
| |
| send pid, {self(), "Kermit!"} |
| receive do |
| {:ok, message} -> |
| IO.puts message |
| end |
Run it in IEx:
| iex> c("spawn2.exs") |
| Hello, World! |
| .... just sits there .... |
The first message is sent back, but the second is nowhere to be seen. What’s worse, IEx just hangs, and we have to use ^C (the Control-C key sequence) to get out of it.
That’s because our greet function handles only a single message. Once it has processed the receive, it exits. As a result, the second message we send it is never processed. The second receive at the top level then just hangs, waiting for a response that will never come.
Let’s at least fix the hanging part. We can tell receive that we want to time out if a response is not received in so many milliseconds. This uses a pseudopattern called after.
| defmodule Spawn3 do |
| def greet do |
| receive do |
| {sender, msg} -> |
| send sender, { :ok, "Hello, #{msg}" } |
| end |
| end |
| end |
| |
| # here's a client |
| pid = spawn(Spawn3, :greet, []) |
| |
| send pid, {self(), "World!"} |
| receive do |
| {:ok, message} -> |
| IO.puts message |
| end |
| |
| send pid, {self(), "Kermit!"} |
| receive do |
| {:ok, message} -> |
| IO.puts message |
» | after 500 -> |
» | IO.puts "The greeter has gone away" |
| end |
| iex> c("spawn3.exs") |
| Hello, World! |
| ... short pause ... |
| The greeter has gone away |
| [Spawn3] |
But how would we make our greet function handle multiple messages? Our natural reaction is to make it loop, doing a receive on each iteration. Elixir doesn’t have loops, but it does have recursion.
| defmodule Spawn4 do |
| def greet do |
| receive do |
| {sender, msg} -> |
| send sender, { :ok, "Hello, #{msg}" } |
» | greet() |
| end |
| end |
| end |
| |
| # here's a client |
| pid = spawn(Spawn4, :greet, []) |
| send pid, {self(), "World!"} |
| receive do |
| {:ok, message} -> |
| IO.puts message |
| end |
| |
| send pid, {self(), "Kermit!"} |
| receive do |
| {:ok, message} -> |
| IO.puts message |
| after 500 -> |
| IO.puts "The greeter has gone away" |
| end |
Run this, and both messages are processed:
| iex> c("spawn4.exs") |
| Hello, World! |
| Hello, Kermit! |
| [Spawn4] |
The greet function might have worried you a little. Every time it receives a message, it ends up calling itself. In many languages, that adds a new frame to the stack. After a large number of messages, you might run out of memory.
This doesn’t happen in Elixir, as it implements tail-call optimization. If the last thing a function does is call itself, there’s no need to make the call. Instead, the runtime simply jumps back to the start of the function. If the recursive call has arguments, then these replace the original parameters. But beware—the recursive call must be the very last thing executed. For example, the following code is not tail recursive:
| def factorial(0), do: 1 |
| def factorial(n), do: n * factorial(n-1) |
While the recursive call is physically the last thing in the function, it is not the last thing executed. The function has to multiply the value it returns by n.
To make it tail recursive, we need to move the multiplication into the recursive call, and this means adding an accumulator:
| defmodule TailRecursive do |
| def factorial(n), do: _fact(n, 1) |
| defp _fact(0, acc), do: acc |
| defp _fact(n, acc), do: _fact(n-1, acc*n) |
| end |