xtc_lrlock(3)
---xtc_lrlock(3)
left-right lock with wait-free reads
| XTC_LRLOCK(3) | Library Functions Manual | XTC_LRLOCK(3) |
NAME
xtc_lrlock_create,
xtc_lrlock_create_ex,
xtc_lrlock_destroy,
xtc_lrlock_read_begin,
xtc_lrlock_read_end,
xtc_lrlock_write_begin,
xtc_lrlock_apply_op,
xtc_lrlock_publish,
xtc_lrlock_publish_full_sync,
xtc_lrlock_write_end,
xtc_lrlock_read_data,
xtc_lrlock_write_data,
xtc_lrlock_mark_ready,
xtc_alrlock_create,
xtc_alrlock_create_ex —
left-right lock with wait-free reads
SYNOPSIS
#include <xtc.h>
#include <xtc_lrlock.h>
int
xtc_lrlock_create(size_t
data_size,
xtc_lrlock_apply_fn
apply_fn,
xtc_lrlock_sync_fn
sync_fn, const char
*name, xtc_lrlock_t
**out);
int
xtc_lrlock_create_ex(const
xtc_lrlock_opts_t *opts,
xtc_lrlock_t **out);
int
xtc_alrlock_create(size_t
data_size,
xtc_lrlock_apply_fn
apply_fn,
xtc_lrlock_sync_fn
sync_fn, const char
*name, xtc_lrlock_t
**out);
int
xtc_alrlock_create_ex(const
xtc_lrlock_opts_t *opts,
xtc_lrlock_t **out);
void
xtc_lrlock_destroy(xtc_lrlock_t
*lr);
const void *
xtc_lrlock_read_begin(xtc_lrlock_t
*lr);
void
xtc_lrlock_read_end(xtc_lrlock_t
*lr);
void *
xtc_lrlock_write_begin(xtc_lrlock_t
*lr);
void
xtc_lrlock_apply_op(xtc_lrlock_t
*lr, const void
*op, size_t
op_size);
void
xtc_lrlock_publish(xtc_lrlock_t
*lr);
void
xtc_lrlock_publish_full_sync(xtc_lrlock_t
*lr);
void
xtc_lrlock_write_end(xtc_lrlock_t
*lr);
const void *
xtc_lrlock_read_data(xtc_lrlock_t
*lr);
void *
xtc_lrlock_write_data(xtc_lrlock_t
*lr);
void
xtc_lrlock_mark_ready(xtc_lrlock_t
*lr);
DESCRIPTION
xtc_lrlock is a wait-free-read concurrency
primitive that maintains two copies of the protected state and atomically
swaps which copy is current on each writer publish. Readers never block;
writers serialise. Ported from the PostgreSQL lrlck branch.
Two copies of the user data live side by side. An atomic read_idx identifies which copy is currently the read copy.
- Set this thread's bit in the active-readers bitmask.
- Bump this thread's epoch counter to odd.
- Sequentially-consistent fence.
- Atomic-acquire-load the current read_idx.
- Read data [read_idx].
- Bump epoch back to even. Clear bitmask bit.
- Take writer mutex (one writer at a time).
- Mutate the inactive copy via
xtc_lrlock_apply_op() or by writing to the buffer returned byxtc_lrlock_write_begin(). xtc_lrlock_publish(): swap read_idx, fence, snapshot epoch counters, wait until each previously-active reader has departed, then re-apply the operations to the now-stale copy so both sides are in sync.xtc_lrlock_write_end() releases the writer mutex.
xtc_lrlock_create()
opens a lock with default options (64 reader slots, 4 KiB oplog).
xtc_lrlock_create_ex()
takes xtc_lrlock_opts_t explicitly, exposing knobs for
max readers, oplog capacity, and flags (notably
XTC_LRLOCK_COW) which lazily allocates the second
copy and madvise(2) 's it back to the OS after each
publish in COW mode .
xtc_alrlock_create()
and
xtc_alrlock_create_ex()
are exact aliases of the corresponding
xtc_lrlock_create() functions, named to make
fiber-awareness explicit at the call site. Reads are wait-free and never
block, so only the writer's publish waits (for in-flight readers to advance
past the version swap); when the writer runs inside a fiber on an
xtc_loop(3), that wait yields the fiber back to its loop
instead of spinning the OS thread, so reader fibers get to run and the loop
keeps serving other work. This behavior is already automatic in
xtc_lrlock_publish(); an object created with either
family works with either family's calls.
xtc_lrlock_apply_op()
records a deterministic operation and applies it immediately to the writer's
copy. On
xtc_lrlock_publish(),
the same op is replayed on the now-stale copy. Operations must be
deterministic (same op + same starting state -> same end state). For
non-deterministic mutations, use
xtc_lrlock_publish_full_sync()
which copies the entire data block instead of replaying ops.
xtc_lrlock_read_data()
and
xtc_lrlock_write_data()
return direct pointers to the read and write copies; they are only safe to
use while the caller holds writer ownership (between
xtc_lrlock_write_begin(and)
xtc_lrlock_write_end()).
xtc_lrlock_mark_ready()
tells the lock that both copies were pre-initialised to the same state,
skipping the first-publish full-sync.
CHOOSING vs. xtc_lwlock
Use xtc_lrlock when reads dominate
(>50:1 R/W) and must never block. Memory cost is 2x the data size unless
XTC_LRLOCK_COW is enabled. See
xtc_lwlock(3) for short-critical-section reader/writer
semantics with one copy.
EXAMPLES
struct config { int max_clients; int log_level; };
struct op_set_max { int new_max; };
static void apply_set_max(void *data, const void *op, size_t sz) {
((struct config *)data)->max_clients =
((const struct op_set_max *)op)->new_max;
(void)sz;
}
static void copy_config(void *dst, const void *src, size_t sz) {
memcpy(dst, src, sz);
}
xtc_lrlock_t *cfg_lock;
xtc_lrlock_create(sizeof(struct config), apply_set_max, copy_config,
"config", &cfg_lock);
/* Reader -- wait-free. */
const struct config *c = xtc_lrlock_read_begin(cfg_lock);
int n = c->max_clients;
xtc_lrlock_read_end(cfg_lock);
/* Writer -- serialised. */
xtc_lrlock_write_begin(cfg_lock);
struct op_set_max op = { .new_max = 100 };
xtc_lrlock_apply_op(cfg_lock, &op, sizeof op);
xtc_lrlock_publish(cfg_lock);
xtc_lrlock_write_end(cfg_lock);
SEE ALSO
HISTORY
Ported from PostgreSQL's lrlck branch. Appeared in xtc 0.1.
SEE ALSO
Pedro Ramalhete and Andreia Correia, ‘Left-Right: A Concurrency Control Technique with Wait-Free’ ‘Population Oblivious Reads’ (2014).
| May 28, 2026 | Debian |