Erlang style processes in Common Lisp
shenanigans at sonic.net
shenanigans at sonic.net
Mon Aug 3 15:12:36 UTC 2015
Creators of Erlang have a Lisp background, and one feature of the Erlang
VM (BEAM) that I'd like back-ported into Common Lisp is their process.
An Erlang "process" is cheap to create, cheap to destroy, cheap when
blocked, and upon exit performs bulk gc of its allocated memory; e.g.,
munmap().
Handling tens of thousands of requests per second per node isn't
uncommon, and these often have *several* workers per request or
connection: hundreds of thousands of processes. Under such scenarios,
anything less than this approach to lightweight processes might suffer
from stalls during long gc runs that would be avoided or significantly
reduced under Erlang's model.
How might we get equivalent cheap ephemeral processes into a
contemporary Common Lisp implementation?
For those unfamiliar with it, what Erlang means by "process" stems from
an implementation of Actor Model with caveats. Each process is more
co-routine than thread, and many may run within the same OS thread
managed by the Erlang scheduler. The BEAM VM pre-empts processes via
"reduction" counting, which may be understood as unit of VM time. Their
famed tag line, "Let it crash," may be loosely understood in CL terms as
an implicit HANDLER-CASE.
The open question here is to address a stated non-goal of CL-MUPROC, "we
rely on the CL implementation's MP system" and "considerably heavier
than Erlang processes". [See presentation link from
https://common-lisp.net/project/cl-muproc/ ]
Some Erlang-on-CL packages use OS threads or in the case of
Erlang-in-Lisp, fork(). While easier for a library author to implement,
these contradict the definition of cheap here. Other such packages punt
the issue altogether and instead focus only on pattern-matching aspects
of Erlang the language.
Then there's Lisp-Flavoured-Erlang, and Elixir offers hygienic macros
that manipulate the full AST properly-- all play nice on same Erlang VM
at runtime-- for people simply looking to avoid Erlang syntax or
semantics. But let's focus on cheap Erlang style processes in Common
Lisp here, please.
1. Semantics of library elements are straight-forward enough (e.g.,
SPAWN, EXIT) and may be borrowed wholesale and safely named within their
own package.
2. Memory allocation & garbage collection:
Erlang BEAM VM doesn't begin to reclaim memory until very late, so an
initial implementation here might assume ephemeral worker processes and
omit gc until exit of a worker. However, some processes are long-lived
in practice.
One compromise might be acceptable: one nursery per process, but
anything promoted to higher generations gets handled however your gc
does it now.
This states nothing about use of shared versus multiple heaps across OS
threads, so such matters may continue to be implementation-dependent.
3. Co-routines:
For something similar to Erlang's reductions, there would need to be a
measure of runtime complexity per process.
However, I've used single thread co-routines for near realtime systems
in C and CL with a loop of function pointers and ensuring that each
referenced function executes under some threshold determined through
experimentation and confirmed via test cases. No pre-empting needed.
While fragile and not easily portable across different hardware
(including same architecture), this may be acceptable for a preliminary
draft.
Using CL-MUPROC routines as an example and extending MUPROCN: perhaps
its body becomes this top-level list of functions from which
interleaving across processes may occur. Then add variations for playing
well with DO or LOOP semantics.
4. Message-passing:
SBCL's sb-concurrency extension with Queue and Mailbox (implementation
of "Optimistic FIFO Queue") can be the solution here too. More aligned
with CL principles, allowing for multiple mailboxes-- and therefore
priority messaging-- would be a welcome advancement beyond Erlang.
(Erlang allows only one mailbox per process: sequential but with pattern
matching, nothing out-of-band...)
An important implementation detail that simplifies gc and increases
cache locality across NUMA nodes: messages get duplicated when
delivered-- each process only references its own copy!
5. (Almost) nothing shared:
Erlang enforces strict prohibition against shared memory, and common
practice is to use an in-memory database (ETS) as key/value store in
lieu of globals. Scala allows but discourages shared memory.
A CL-inspired take on this might be to use SBCL's approach with thread
creation: upon creating a new process, you get: A) global special
values, but modify at own risk... B) LET bindings are local to each
process; C) threads don't inherit dynamic bindings from parent. i.e.,
http://sbcl.org/manual/index.html#Special-Variables
6. Scheduling processes on OS threads:
This is delicate in Erlang, and I've experienced specialized use cases
interfering with their scheduler when under heavy load. Instead for our
purposes, let the programmer handle the mapping of processes to OS
threads. Less is more here.
7. Finally, gen_server & OTP fiends may be implemented as their own
packages... or skipped entirely!
Thoughts on feasibility?
Thanks,
-Daniel
More information about the pro
mailing list