Processes

(Note: “Process” here refers to the paragram process model - this is not necessarily the same thing that your OS calls a “Process”)

A process in paragram is a unit of concurrency, just like erlang.

Processes have a mailbox, and they can send messages to other processes. Processes should not share state, although this is not prohibited. The basic lifecycle of a process is:

  • get next message (or wait until a message is received)
  • handle message
  • repeat

This means that while handling a message, a process cannot be interrupted, so a process need not protect its internals with a lock or any other thread-safety measures.

It also means that messages should not be considered to be immediate - they can be queued for an arbitrary amount of time, depending on how busy the receiving process is. Because of this, you should make sure that your process never blocks indefinitely - if you do, messages will never be delivered, including termination messages - and then your app won’t shut down.

As an implementation detail, a paragram process can either be implemented as a python thread or as a separate OS process. You should use threads where possible, as they are much more efficient. You may prefer to use processes in the following cases:

  • you need concurrency across multiple cores, and python’s GIL prevents that
  • you need to protect the current process from state modification by subprocesses. autonose does this to ensure that imported modules are not cached between test runs.

You can specify what kind of process should be created by passing the kind keyword argument to either spawn() or spawn_link().

Note

OS-based processes may not work on windows, as paragram relies on the multiprocessing library to create new processes.

Spawning new processes

Any existing process can spawn a new process. When creating the first process, you can use paragram.main. Here’s a simple quickstart that shows how you can start a process and send it a message, and how the process handles a message.

>>> import paragram as pg
>>> import time
>>> def init_process(proc):
...     print "process started!"
...     @proc.receive("go away")
...     def go_away(msg):
...         print "process ending."
...         proc.terminate()
...
>>> proc = pg.main.spawn_link(target=init_process, name="my first process"); time.sleep(0.5)
process started!
>>> proc.send("go away")
>>> proc.wait()
process ending.

You should spawn processes using spawn_link(). This creates a _linked_ process. If you want to create an unlinked process, you can use spawn(). See Terminating for more details.

Handling Messages

Messages in erlang are handled via pattern matching. Python doesn’t have pattern matching as part of the language, but we can do a decent job for most cases.

Messages can be matched on by equality or by type. For example:

>>> proc.receiver["hello"] = some_func

Will cause some_func to handle any message that is ("hello",).

>>> proc.receiver[str, int] = some_func

Will match any message that consists of a string and an integer. For example, ("hello", 1) or ("two", 2).

For the full documentation of pattern matching, see Pattern Matching.

When a receiver’s pattern matches, it is called with the arguments of the message. For example, when the message ("hello", 123) is matched, the associated function gets both arguments, so it should have the following signature:

>>> def handle(msg, num):
...     # ...

There is no support for keyword arguments in messages.

When setting up a process’ receivers, you have two syntaxes available:

>>> proc.receive[pattern] = callable

and:

>>> @proc.receive(pattern)
... def callable(\*args):
...     # ...

The first syntax is mostly useful for (re)using an existing callable.

But if the receiver is only going to be used once, it can be convenient to define it inline, as in the second example.

Terminating

At any point, you can call terminate() on a process to send it an exit message.

While inside a message handler, you can also raise paragram.Exit if you don’t have a reference to the current procss - it will have the same effect.

At any point, if a process receives a message that matches no receivers, it will terminate with an UnhandledMessage exception.

When a process terminates, it sends a message of the form (exit, process) to each of its linked processes.

By default, a process receiving such a message will itself terminate. But you can override that behaviour like so:

>>> import paragram as pg
>>> @proc.receive(pg.Exit, pg.Process)
>>> def linked_terminated(exit, proc):
...     pass # handle however you like

Note that the exit paramater here is of type paragram.Exit, and so has a error attribute. This is set to the exception instance in the case of a failure, or None when a process is terminated normally via terminate()

If you don’t want to be informed when a process terminates, you should use spawn() instead of spawn_link().

Table Of Contents

Previous topic

What is paragram?

Next topic

Pattern Matching

This Page