Monday, January 30, 2017

Elixir - Processes

In Elixir, all code runs inside processes. Processes are isolated from each other, run concurrent to one another and communicate via message passing. Elixir’s processes should not be confused with operating system processes. Processes in Elixir are extremely lightweight in terms of memory and CPU (unlike threads in many other programming languages). Because of this, it is not uncommon to have tens or even hundreds of thousands of processes running simultaneously.

In this chapter, we will learn about the basic constructs for spawning new processes, as well as sending and receiving messages between different processes.

spawn

The easiest way to create a new process is to use the spawn function. The spawn accepts a function that will be run in the new process. For example:
pid = spawn(fn -> 2 * 2 end)
Process.alive?(pid)
When running the above program, it produces following result:
false
The return value of the spawn function is a PID. This is a unique identifier for the process and so if you run the code above your PID will be different from mine. As you can see in this example the process is dead when we check to see if it alive. This is because the process will exit as soon as it has finished running the given function.
As already mentioned, all elixir code runs inside processes. If you run the self function you will see the PID for your current session:
pid = self
 
Process.alive?(pid)
When running the above program, it produces following result:
true

Message passing

We can send messages to a process with send and receive them with receive. Let us pass a message to the current process and receive it on the same.
send(self(), {:hello, "Hi people"})

receive do
    {:hello, msg} -> IO.puts(msg)
    {:another_case, msg} -> IO.puts("This one won't match!")
end
When running the above program, it produces following result:
Hi people
We sent a message to the current process using the send function and passing it the PID of self. Then we handled the incoming message using receive function.
When a message is sent to a process, the message is stored in the process mailbox. The receive block goes through the current process mailbox searching for a message that matches any of the given patterns. receive supports guards and many clauses, such as case.
If there is no message in the mailbox matching any of the patterns, the current process will wait until a matching message arrives. A timeout can also be specified. For example,
receive do
    {:hello, msg}  -> msg
after
    1_000 -> "nothing after 1s"
end
When running the above program, it produces following result:
nothing after 1s
NOTE: A timeout of 0 can be given when you already expect the message to be in the mailbox.

Links

The most common form of spawning in Elixir is actually via spawn_link function. Before taking a look at an example with spawn_link, let’s try to see what happens when a process fails.
spawn fn -> raise "oops" end
When running the above program, it produces following error:
[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
    :erlang.apply/2
It logged an error but the spawning process is still running. That’s because processes are isolated. If we want the failure in one process to propagate to another one, we need to link them. This can be done with spawn_link function. Lets llok at an example:
spawn_link fn -> raise "oops" end
When running the above program, it produces following error:
** (EXIT from #PID<0.41.0>) an exception was raised:
    ** (RuntimeError) oops
        :erlang.apply/2
If you are running this in iex shell then the shell handles this error and does not exit. But if you run by first making a script file and then using elixir <file-name>.exs, the parent process itself would also be brought down due to this failure.
Processes and links play an important role when building fault-tolerant systems. In Elixir applications, we often link our processes to supervisors which will detect when a process dies and start a new process in its place. This is only possible because processes are isolated and don’t share anything by default. And since processes are isolated, there is no way a failure in a process will crash or corrupt the state of another. While other languages would require us to catch/handle exceptions, in Elixir we are actually fine with letting processes fail because we expect supervisors to properly restart our systems.

State

If you are building an application that requires state, for example, to keep your application configuration, or you need to parse a file and keep it in memory, where would you store it? Elixir's process functionality can come in handy when doing such things.
We can write processes that loop infinitely, maintain state, and send and receive messages. As an example, let’s write a module that starts new processes that work as a key-value store in a file named kv.exs.
defmodule KV do
  def start_link do
    Task.start_link(fn -> loop(%{}) end)
  end

  defp loop(map) do
    receive do
      {:get, key, caller} ->
        send caller, Map.get(map, key)
        loop(map)
      {:put, key, value} ->
        loop(Map.put(map, key, value))
    end
  end
end
Note that the start_link function starts a new process that runs the loop function, starting with an empty map. The loop function then waits for messages and performs the appropriate action for each message. In the case of a :get message, it sends a message back to the caller and calls loop again, to wait for a new message. While the :put message actually invokes loop with a new version of the map, with the given key and value stored.
Let’s try it by running:
iex kv.exs
Now you should be in your iex shell. To test out our module, try the following:
{:ok, pid} = KV.start_link

# pid now has the pid of our new process that is being 
# used to get and store key value pairs 

# Send a KV pair :hello, "Hello" to the process
send pid, {:put, :hello, "Hello"}

# Ask for the key :hello
send pid, {:get, :hello, self()}

# Print all the received messages on the current process.
flush()
When running the above program, it produces following result:
"Hello"

No comments:

Post a Comment