Although a PID is displayed as three numbers, it contains just two fields; the first number is the node ID and the next two numbers are the low and high bits of the process ID. When you run a process on your current node, its node ID will always be zero. However, when you export a PID to another node, the node ID is set to the number of the node on which the process lives.
That works well once a system is up and running and everything is knitted together. If you want to register a callback process on one node and an event-generating process on another, just give the callback PID to the generator.
But how can the callback find the generator in the first place? One way is for the generator to register its PID, giving it a name. The callback on the other node can look up the generator by name, using the PID that comes back to send messages to it.
Here’s an example. Let’s write a simple server that sends a notification about every 2 seconds. To receive the notification, a client has to register with the server. And we’ll arrange things so that clients on different nodes can register.
While we’re at it, we’ll do a little packaging so that to start the server you run Ticker.start, and to start the client you run Client.start. We’ll also add an API Ticker.register to register a client with the server.
Here’s the server code:
| defmodule Ticker do |
| |
| @interval 2000 # 2 seconds |
| @name :ticker |
| |
| def start do |
| pid = spawn(__MODULE__, :generator, [[]]) |
| :global.register_name(@name, pid) |
| end |
| def register(client_pid) do |
| send :global.whereis_name(@name), { :register, client_pid } |
| end |
| |
| def generator(clients) do |
| receive do |
| { :register, pid } -> |
| IO.puts "registering #{inspect pid}" |
| generator([pid|clients]) |
| after |
| @interval -> |
| IO.puts "tick" |
| Enum.each clients, fn client -> |
| send client, { :tick } |
| end |
| generator(clients) |
| end |
| end |
| end |
We define a start function that spawns the server process. It then uses :global.register_name to register the PID of this server under the name :ticker.
Clients who want to register to receive ticks call the register function. This function sends a message to the Ticker server, asking it to add those clients to its list. Clients could have done this directly by sending the :register message to the server process. Instead, we give them an interface function that hides the registration details. This helps decouple the client from the server and gives us more flexibility to change things in the future.
Before we look at the actual tick process, let’s stop to consider the start and register functions. These are not part of the tick process—they are simply chunks of code in the Ticker module. This means they can be called directly wherever we have the module loaded—no message passing required. This is a common pattern; we have a module that is responsible both for spawning a process and for providing the external interface to that process.
Back to the code. The last function, generator, is the spawned process. It waits for two events. When it gets a tuple containing :register and a PID, it adds the PID to the list of clients and recurses. Alternatively, it may time out after 2 seconds, in which case it sends a {:tick} message to all registered clients.
(This code has no error handling and no means of terminating the process. I just wanted to illustrate passing PIDs and messages between nodes.) The client code is simple:
| defmodule Client do |
| |
| def start do |
| pid = spawn(__MODULE__, :receiver, []) |
| Ticker.register(pid) |
| end |
| |
| def receiver do |
| receive do |
| { :tick } -> |
| IO.puts "tock in client" |
| receiver() |
| end |
| end |
| end |
It spawns a receiver to handle the incoming ticks, and passes the receiver’s PID to the server as an argument to the register function. Again, it’s worth noting that this function call is local—it runs on the same node as the client. However, inside the Ticker.register function, it locates the node containing the server and sends it a message. As our client’s PID is sent to the server, it becomes an external PID, pointing back to the client’s node.
The spawned client process simply loops, writing a cheery message to the console whenever it receives a tick message.
Let’s run it. We’ll start up our two nodes. We’ll call Ticker.start on node one. Then we’ll call Client.start on both node one and node two.
Window #1 | ||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||
Window #2 | ||||||||||||||||||||||||||||||||||||||||||
|
To stop this, you’ll need to exit IEx on both nodes.
When you name something, you are recording some global state. And as we all know, global state can be troublesome. What if two processes try to register the same name, for example?
The runtime has some tricks to help us. In particular, we can list the names our application will register in the app’s mix.exs file. (We’ll cover how when we look at packaging an application.) However, the general rule is to register your process names when your application starts.