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.
As already mentioned, all elixir code runs inside processes. If you run the self function you will see the PID for your current session:
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,
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.
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.
Let’s try it by running:
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:
falseThe 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!") endWhen running the above program, it produces following result:
Hi peopleWe 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" endWhen running the above program, it produces following result:
nothing after 1sNOTE: 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" endWhen running the above program, it produces following error:
[error] Process #PID<0.58.00> raised an exception ** (RuntimeError) oops :erlang.apply/2It 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" endWhen running the above program, it produces following error:
** (EXIT from #PID<0.41.0>) an exception was raised: ** (RuntimeError) oops :erlang.apply/2If 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 endNote 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.exsNow 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