Processes and messages
Addressable, mailbox-owning units with private state -- the Erlang/BEAM model, in C.
---A bare coroutine computes and returns. A process is a coroutine with
an identity (a xtc_pid_t) and a mailbox: other processes address it
by pid and communicate only by sending it messages. Nothing is shared;
there are no locks on a process’s state because no one else can touch it.
This is the Erlang/BEAM model, in C.
Spawn, send, receive
xtc_proc_spawn(loop, fn, arg, opts, &pid) starts fn(arg) as a process
and gives you back its pid. Inside a process, xtc_self() returns your
own pid, xtc_send(pid, data, size) copies size bytes into another
process’s mailbox, and xtc_recv(&buf, &size, timeout_ns) blocks
(suspends the fiber) until a message arrives or the timeout elapses.
Here is a two-process ping/pong. pong waits for a number and replies
with one more; ping kicks it off and bounces the number back until it
reaches a limit.
sequenceDiagram
participant P as ping
participant Q as pong
P->>Q: {from: ping, n: 0}
Q->>P: {from: pong, n: 1}
P->>Q: {from: ping, n: 2}
Q->>P: {from: pong, n: 3}
P->>Q: {from: ping, n: 4}
Note over Q: n >= ROUNDS, done
Each arrow is one xtc_send into the target’s mailbox; each process
sits in xtc_recv until a message arrives. No shared memory, no locks.
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "xtc.h"
#include "xtc_loop.h"
#include "xtc_proc.h"
#define ROUNDS 4
/* Messages are plain bytes. xtc_recv gives no sender, so we carry the
* reply address in the payload -- the usual libxtc idiom. */
struct msg {
xtc_pid_t from;
int n;
};
static void
pong(void *arg)
{
void *raw;
size_t sz;
(void)arg;
for (;;) {
struct msg req, reply;
if (xtc_recv(&raw, &sz, 1000LL * 1000 * 1000) != XTC_OK)
return; /* timed out: no partner left */
if (sz != sizeof req) {
xtc_free(raw);
continue;
}
memcpy(&req, raw, sizeof req);
xtc_free(raw); /* received buffers are ours to free */
if (req.n >= ROUNDS) {
printf("pong: reached %d, done\n", req.n);
return;
}
reply.from = xtc_self();
reply.n = req.n + 1;
(void)xtc_send(req.from, &reply, sizeof reply);
}
}
static void
ping(void *arg)
{
xtc_pid_t peer = *(xtc_pid_t *)arg;
struct msg first = { xtc_self(), 0 };
void *raw;
size_t sz;
(void)xtc_send(peer, &first, sizeof first);
for (;;) {
struct msg r;
if (xtc_recv(&raw, &sz, 1000LL * 1000 * 1000) != XTC_OK)
return;
memcpy(&r, raw, sizeof r);
xtc_free(raw);
printf("ping: got %d\n", r.n);
if (r.n >= ROUNDS)
return;
r.from = xtc_self();
(void)xtc_send(peer, &r, sizeof r);
}
}
int
main(void)
{
xtc_loop_t *loop;
xtc_pid_t pong_pid;
if (xtc_loop_init(&loop) != XTC_OK)
return 1;
if (xtc_proc_spawn(loop, pong, NULL, NULL, &pong_pid) != XTC_OK)
return 1;
if (xtc_proc_spawn(loop, ping, &pong_pid, NULL, NULL) != XTC_OK)
return 1;
(void)xtc_loop_run(loop);
(void)xtc_loop_fini(loop);
return 0;
}
Tested source: docs/_includes/snippets/03_ping_pong.c
ping: got 1
ping: got 3
pong: reached 4, done
Three things to notice
Messages are copies. xtc_send copies the bytes into the
recipient’s mailbox. The sender and receiver never share the buffer, so
there is nothing to lock and no lifetime to coordinate across processes.
A received buffer is yours to free. xtc_recv hands you a
heap buffer that you own. Release it with
xtc_free
– not plain free. libxtc may be running under a custom allocator (an
embedder like PostgreSQL installs one), and freeing an
allocator-supplied buffer with the C library free is a mismatched-free
bug. Every libxtc call that returns a caller-owned buffer documents
xtc_free; that man page lists them.
There is no sender field. xtc_recv does not tell you who sent the
message. If you need to reply, put your own pid in the payload – that
is what the from field in the example does. This keeps the mailbox a
plain byte queue and lets you design your own protocols on top.
Shared state behind a mutex. The C default is a struct guarded by a
pthread_mutex. It is faster for a single hot counter, but it does not compose: every new invariant adds another lock, lock order becomes a global proof obligation, and a thread that dies holding a lock wedges everyone. The process model trades a little copy cost for the property that state has exactly one owner and failure is contained to that owner. libxtc still ships mutexes, rwlocks, RCU, and a lock manager (Locks and synchronization) for the cases that genuinely want shared memory – but the default unit of concurrency is the shared-nothing process.
Selective receive
Sometimes a process wants the next message that matches a predicate,
leaving others in the mailbox for later. xtc_recv_match(match_fn,
user_data, &buf, &size, timeout) scans the mailbox and returns the
first message for which match_fn returns non-zero, preserving the
arrival order of the rest. This is how you implement a request/response
correlation (pull the reply with your request id) without draining
unrelated traffic. See
xtc_proc(3).
What you have learned
- A process is an addressable, mailbox-owning coroutine with private state.
xtc_sendcopies;xtc_recv/xtc_recv_matchreceive; received buffers are freed withxtc_free.- Replies carry the sender pid in the payload by convention.
Processes let things run independently. The next chapter is about what happens when one of them fails, and how to build systems that recover: links, monitors, and supervisors.
← Fibers and the event loop · Next: Links, monitors, and supervisors →