📜 ⬆️ ⬇️

Erlang and its processes

0 Preamble


Model is not the world. As humans, we cannot fully understand reality. We can only build its model and through it study and use the real world. The completeness, success, vitality of a part of reality in the information space (or in our head) depends on which model we choose.

Each programming language has its own paradigm of building reality. In functional languages, the calculation process is interpreted as calculating the values ​​of functions, in imperative languages, on the contrary, the computational process is described in the form of instructions that change the state of the program.

In this article, the author will highlight the functional programming language Erlang, the paradigm of which may sound like this: “everything is a process”. In the first part of this article, introductory information will be given on the creation and communication of processes between each other, in the second we will focus on planning processes inside the Erlang virtual machine and on specification of processes. The article is intended for beginners who want to start building complex, multi-threaded and fault tolerant applications in the Erlang language.

1 Work with processes


Erlang language implements a lightweight model of processes that run in a virtual machine - BEAM (Bogdan / Björn's Erlang Abstract Machine), this model allows you to:

1.1 Creating processes

The following functions are used to create processes (the number of arguments that the function can take is indicated through slash):

In addition to these functions in the documentation [1] you can find many functions intended for maintenance and manipulation of processes.
Now let's create a simple process and run it. We launch the interactive shell and execute the following commands:
')
Eshell V5.8 (abort with ^G)
1> Fun = fun() -> receive after infinity -> ok end end.
#Fun<erl_eval.20.67289768>
2> Pid = spawn(Fun).
<0.33.0>
3>


In the first line, we create a function that will be the body of the process, then we create the process itself using the spawn / 1 function, the process has been created and assigned the identifier <0.33.0>. Now let's call the function c: i / 0 (c is the module in which the functions for interactive work in the Erlang shell are collected):

3> i().
Pid Initial Call Heap Reds Msgs
Registered Current Function Stack
<0.0.0> otp_ring0:start/2 987 2581 0
...
<0.33.0> erlang:apply/2 233 18 0
erl_eval:receive_clauses/8 10


We see that our process works. Also, to monitor the processes, you can run the graphical utility pman:

5> pman:start().
<0.37.0>
6>




1.2 Inter-process communication

Now let's look at the simplest example of how processes can communicate with each other. To do this, we will write a small module (for writing and debugging programs on Erlang, the author uses a bunch of Emacs + Erlang mode + distel):
  1. -module (proc).
  2. -export ([start / 0, p / 0]).
  3. start () ->
  4. spawn (proc, p, []).
  5. p () ->
  6. receive
  7. {Pid, Msg} when is_pid (Pid) ->
  8. io: format ( "Hello from proc: ~ p, mesg: ~ p ~ n" , [Pid, Msg]),
  9. Pid! Msg
  10. p ();
  11. stop ->
  12. ok;
  13. _ ->
  14. io: format ( "Unknown type of message ~ n" , []),
  15. p ()
  16. end.

The structure of the module is quite simple, the first line describes the name of the module, it must match the file name (in this case, the file is called proc.erl), in the second line we export two functions, the first is for creating the process, and the second describes body function. To create a process, we use a version of the spawn function with three arguments, they can be described by a tuple {M, F, A}, where M is the module that contains the function F called with argument list A (in this case, the function has no arguments) . For the process body, the construction is used:
Receive
Pattern1 when Guard1 -> exp-11,...exp-1n;
...
Pattern1 when Guard1 -> exp-m1,...exp-mn
after Time -> exp-k1,...exp-kh
End


Where Pattern is a pattern for matching the received message, Guard additional conditions, Time is a timer, specified in ms, it works if the queue is empty for a specified time.

Now we compile our module and try to create a process.

1> c("proc").
{ok,proc}
2> Pid = proc:start().
<0.37.0>
3> Pid ! {self(), "Hello from shell"}.
Hello from proc: <0.30.0>, mesg: "Hello from shell"
{<0.30.0>,"Hello from shell"}
4> flush().
Shell got "Hello from shell"
ok
5>


In the first line, we compile our module, then create a process. The Pid variable stores the process identifier to which, using the! Operator, a message is sent in the form of the tuple {self (), "..."}, where the self / 0 function returns the identifier of the process in which we are located - an interactive shell process.

The created process, having received the message, takes it out of the queue and matches it with one of the templates in this case with {Pid, Msg}, and then sends the received message back. The flush / 0 function in this case is needed to reset all messages sent to the interactive shell, this is necessary because this process does not have a receive block.

As a practice, I suggest that readers create two independent processes that exchange processes, for example, at the command of the Eralang shell, the first sends a pair of numbers to the second, which summarizes them and sends them back to the first, and it already outputs the answer to the shell.

2 Digging deeper



Let's look at a few more questions about processes: first, how the process scheduler works, and second, let's see what information you can learn about the process.

2.1 Process Planner


Planning erlang processes is based on reductions. Reduction in logic and mathematics is a logical-methodological method of reducing complex to simple (Wikipedia). One reduction is approximately equivalent to a function call. The process is allowed to run until it is suspended pending input (messages from another process, in this case the suspended process is in the receive block) or until it consumes N reductions (I'm not sure with the exact number, but found somewhere that it is 1000 reductions).

There are functions with which you can influence the planning process (erlang: yield / 0, erlang: bump_reductions / 1), but they should be used only in rare cases (as stated in the documentation [1], these functions can be changed / deleted in future releases ). A process waiting for a message will be rescheduled as soon as a message appears in its queue or a timer in the receive block is triggered, after which it is placed last in the scheduler queue.

Erlang has 4 queues with different priorities: maximum (max), high (high), normal (normal) and low (low). The scheduler will first look for processes in the queue with a max priority and run them until the queue is empty, then the same with processes in the high queue. Then, provided that max and high no more processes, the scheduler will start processes with normal priority, until the queue becomes empty, or until the process performs a certain number of reductions, after which the scheduler processes the processes with low priority.

The priority of normal and low can change places, for example: you have hundreds of processes with normal priority and several with low, in this case, the scheduler can first execute processes with low priority and only then with normal.

2.2 Internal process information


Among the many functions for working with processes, there is one very interesting one: erlang: process_info / 1 or erlang: process_info / 2, using this function you can get detailed information about the process dictionary, garbage collector, heap size, linked processes and other interesting things (full see the list in the documentation [1]).

The first variant of the function (with one argument) is recommended to be used only for debugging purposes - it gives the complete specification of the process, for the rest it is better to use the second variant.

Let's write a simple function that will display the following information about the process: the registered name; the function by which the process was spawned; list of linked processes.

  1. -module (test).
  2. -export ([info / 1]).
  3. info (pid) ->
  4. Spec = [registered_name, initial_call, links],
  5. case process_info (Pid, Spec) of
  6. undefined ->
  7. undefined;
  8. Result ->
  9. [{pid, pid} | Result]
  10. end.


In line 5 we create a specification according to which information about the process will be obtained. Run the shell, compile the module and test.

3> processes().
[<0.0.0>,<0.3.0>,<0.5.0>,<0.6.0>,<0.8.0>,<0.9.0>,<0.10.0>,
<0.11.0>,<0.12.0>,<0.13.0>,<0.14.0>,<0.15.0>,<0.16.0>,
<0.17.0>,<0.18.0>,<0.19.0>,<0.20.0>,<0.21.0>,<0.23.0>,
<0.24.0>,<0.25.0>,<0.26.0>,<0.30.0>]
4> test:info(pid(0,0,0)).
[{pid,<0.0.0>},
{registered_name,init},
{initial_call,{otp_ring0,start,2}},
{links,[<0.5.0>,<0.6.0>,<0.3.0>]}]
5>


In line 3 we get a list of all running processes, then we call our function, which shows that the registered name of the process is <0.0.0> - init, it is generated by calling otp_ring0: start / 2 and three other processes are linked to it.

In the next article, we will look at how to link processes with each other and track their status.

Bibliography


1. Excellent online documentation .
2. Basic information about the processes .
3. On the advanced design of virtual machines .
4. ERLANG Programming by Francesco Cesarini and Simon Thompson
5. About process planning .

Source: https://habr.com/ru/post/114232/


All Articles